clear-react-router 1.9.1 → 1.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/dist/components/Link.d.ts +2 -2
- package/dist/index.js +37 -26
- package/dist/runtime/invalidate.d.ts +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/utils/isCacheItemFresh.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ It provides first-class support for:
|
|
|
31
31
|
- **Prefetching** - Preload data on hover for instant navigation
|
|
32
32
|
- **Lazy Loading** - Code-split your routes with dynamic imports for optimal performance
|
|
33
33
|
- **Scroll Restoration** — Automatically saves and restores scroll position when navigating back to a page (preserves user's scroll position)
|
|
34
|
-
- **
|
|
34
|
+
- **Optimistic navigation** — Instantly renders stale cached data while fresh data is loaded in the background.
|
|
35
35
|
- **Flexible API** - Use components or hooks as you prefer
|
|
36
36
|
- **Browser History** - Full support for browser back/forward buttons
|
|
37
37
|
- **Context-aware** - Pass and update context through routes
|
|
@@ -82,12 +82,13 @@ Normalizes route configuration. Extracts dynamic params, builds nested paths.
|
|
|
82
82
|
| `afterLoad` | `({ params, context, setContext }) => Promise<void>` | Runs after a successful navigation once the route has finished loading. Analytics, side effects after data is loaded. Can update context via `setContext` |
|
|
83
83
|
| `fallback` | `ReactElement \| () => ReactElement` | Loading fallback (for lazy loading) |
|
|
84
84
|
| `loaderFallback` | `ReactElement \| () => ReactElement` | Loading fallback for the route's `loader`. Overrides the global `defaultLoaderFallback` set in `Router` |
|
|
85
|
-
| `retry` | `number \| { count: number; delay: number }` |
|
|
85
|
+
| `retry` | `number \| { count: number; delay: number }` | Overrides the global cache revalidation retry policy for this route |
|
|
86
|
+
| `optimistic` | `boolean \| undefined` | Instant navigation using stale data while fresh data is loaded in the background |
|
|
86
87
|
| `errorElement` | `ReactElement \| () => ReactElement` | Error fallback for the route. Overrides the global `defaultErrorElement` set in `Router` |
|
|
87
88
|
| `staleTime` | `number` | Time in milliseconds before cached loader data is considered stale. Overrides Router.defaultStaleTime. If neither value is provided, cached data never expires |
|
|
88
89
|
| `actions` | `({ params, context, invalidate, setContext }) => Record<string, (formData: FormData) => unknown \| Promise<unknown>>` | Defines route actions for data mutations. Actions receive `FormData`, can update context via `setContext`, and can invalidate cached loader data using the router-provided `invalidate` |
|
|
89
|
-
| `pollingInterval` | `number \| undefined` |
|
|
90
|
-
| `preserveScroll` | `boolean \| undefined` |
|
|
90
|
+
| `pollingInterval` | `number \| undefined` | Polling interval (in milliseconds) for automatically revalidating data while the route is active |
|
|
91
|
+
| `preserveScroll` | `boolean \| undefined` | Save and restore route scroll position when navigating between pages |
|
|
91
92
|
|
|
92
93
|
Loader arguments:
|
|
93
94
|
```ts
|
|
@@ -113,7 +114,7 @@ Component for client-side navigation with prefetch support, active state detecti
|
|
|
113
114
|
| `style` | `CSSProperties \| ({ isActive, isPending }) => CSSProperties` | `undefined` | Inline styles. Can be a function for dynamic styling |
|
|
114
115
|
| `activeClassName` | `string` optional | `'active-link'` | Class name applied when the link matches the current URL |
|
|
115
116
|
| `pendingClassName` | `string` optional | `'pending-link'` | Class name applied when the link's target is loading |
|
|
116
|
-
| `
|
|
117
|
+
| `beforeNavigate` | `() => Promise<void> \| undefined` | `undefined` | Callback fired before navigation |
|
|
117
118
|
|
|
118
119
|
**State values:**
|
|
119
120
|
|
|
@@ -163,6 +164,11 @@ import { Router, Link } from 'clear-react-router';
|
|
|
163
164
|
<Link to="/profile" style={({ isActive }) => ({ fontWeight: isActive ? 'bold' : 'normal' })}>
|
|
164
165
|
Profile
|
|
165
166
|
</Link>
|
|
167
|
+
|
|
168
|
+
// Use `beforeNavigate`
|
|
169
|
+
<Link to="/details" beforeNavigate={saveDashboardData}>
|
|
170
|
+
Admin Panel
|
|
171
|
+
</Link>
|
|
166
172
|
```
|
|
167
173
|
**Important**: prefetch="render" should be used sparingly, as it preloads data immediately when the link is rendered, which may cause unnecessary network requests.
|
|
168
174
|
|
|
@@ -13,7 +13,7 @@ type LinkProps = Omit<ComponentPropsWithoutRef<'a'>, 'href' | 'className' | 'sty
|
|
|
13
13
|
className?: string | ((arg: States) => string);
|
|
14
14
|
activeClassName?: string;
|
|
15
15
|
pendingClassName?: string;
|
|
16
|
-
|
|
16
|
+
beforeNavigate?(): Promise<void>;
|
|
17
17
|
};
|
|
18
|
-
export declare const Link: ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, className, style,
|
|
18
|
+
export declare const Link: ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, className, style, beforeNavigate, activeClassName, pendingClassName, }: LinkProps) => import("react/jsx-runtime").JSX.Element;
|
|
19
19
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -113,13 +113,13 @@ var createCommitNavigation = (navigationExecutor, routeItemDataState) => (nextLo
|
|
|
113
113
|
};
|
|
114
114
|
//#endregion
|
|
115
115
|
//#region utils/isCacheItemFresh.ts
|
|
116
|
-
var createIsCacheItemFresh = (
|
|
116
|
+
var createIsCacheItemFresh = (loaderMap) => ({ routeItem, pathname }) => {
|
|
117
117
|
if (!routeItem) return true;
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
118
|
+
const item = loaderMap.get(pathname);
|
|
119
|
+
if (item === void 0) return false;
|
|
120
120
|
const staleTime = routeItem.staleTime ?? routerConfig.defaultStaleTime;
|
|
121
121
|
if (staleTime === void 0) return true;
|
|
122
|
-
return Date.now() - timestamp <= staleTime;
|
|
122
|
+
return Date.now() - item.timestamp <= staleTime;
|
|
123
123
|
};
|
|
124
124
|
//#endregion
|
|
125
125
|
//#region ../../node_modules/react/cjs/react-jsx-runtime.production.js
|
|
@@ -216,9 +216,9 @@ var findRoute = (pathname, includeAll) => {
|
|
|
216
216
|
var navigationSeq = 0;
|
|
217
217
|
var interval = 0;
|
|
218
218
|
var createNavigate = (routerState, revalidateCache) => {
|
|
219
|
-
const { loaderStateRef, scrollMapState, pendingState, contextState,
|
|
219
|
+
const { loaderStateRef, scrollMapState, pendingState, contextState, loaderMap, routeItemDataState } = routerState;
|
|
220
220
|
const commitNavigation = createCommitNavigation(createCommitState(routerState), routeItemDataState);
|
|
221
|
-
const isCacheItemFresh = createIsCacheItemFresh(
|
|
221
|
+
const isCacheItemFresh = createIsCacheItemFresh(loaderMap);
|
|
222
222
|
const getContext = () => ({
|
|
223
223
|
context: contextState.getState(),
|
|
224
224
|
setContext: contextState.setState
|
|
@@ -265,14 +265,23 @@ var createNavigate = (routerState, revalidateCache) => {
|
|
|
265
265
|
[prevPathname]: scrollPosition
|
|
266
266
|
};
|
|
267
267
|
});
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
268
|
+
if (routeItem?.optimistic && loaderMap.has(location.pathname)) {
|
|
269
|
+
routeItemDataState.setState({
|
|
270
|
+
routeItem,
|
|
271
|
+
location
|
|
272
|
+
});
|
|
273
|
+
const currentLoaderState = loaderMap.get(location.pathname)?.state;
|
|
274
|
+
if (currentLoaderState) loaderStateRef.set(currentLoaderState);
|
|
275
|
+
} else {
|
|
276
|
+
const pendingShouldExist = routeItem?.loader && !isCacheItemFresh({
|
|
277
|
+
routeItem,
|
|
278
|
+
pathname: location.pathname
|
|
279
|
+
});
|
|
280
|
+
pendingState.setState(pendingShouldExist ? {
|
|
281
|
+
routeItem,
|
|
282
|
+
location
|
|
283
|
+
} : void 0);
|
|
284
|
+
}
|
|
276
285
|
};
|
|
277
286
|
const afterEachLoad = (routeItem) => {
|
|
278
287
|
if (!routeItem?.pollingInterval) return;
|
|
@@ -319,10 +328,10 @@ var createNavigate = (routerState, revalidateCache) => {
|
|
|
319
328
|
//#endregion
|
|
320
329
|
//#region runtime/invalidate.ts
|
|
321
330
|
var redirect = () => Promise.resolve();
|
|
322
|
-
var createInvalidate = ({ routeItemDataState, loaderStateRef,
|
|
331
|
+
var createInvalidate = ({ routeItemDataState, loaderStateRef, loaderMap, currentLoaderState, contextState }, revalidateCache) => {
|
|
323
332
|
const invalidatePath = async (routeItem, pathname, options) => {
|
|
324
333
|
const routePathname = routeItemDataState.getState().location.pathname;
|
|
325
|
-
|
|
334
|
+
loaderMap.delete(pathname);
|
|
326
335
|
const params = getParamsObject();
|
|
327
336
|
try {
|
|
328
337
|
if (routeItem?.beforeLoad && options?.withBeforeLoad) {
|
|
@@ -359,7 +368,7 @@ var createInvalidate = ({ routeItemDataState, loaderStateRef, timestampMap, curr
|
|
|
359
368
|
const routeItem = findRoute(pathname);
|
|
360
369
|
if (!routeItem) return [];
|
|
361
370
|
const pathnameArray = [];
|
|
362
|
-
for (const [key] of
|
|
371
|
+
for (const [key] of loaderMap) if (comparePaths(routeItem, key)) pathnameArray.push(key);
|
|
363
372
|
const currentResults = await Promise.all(pathnameArray.map((pathname) => invalidatePath(routeItem, pathname, options)));
|
|
364
373
|
if (!options?.withChildren || !routeItem.children?.length) return currentResults;
|
|
365
374
|
const childResults = await Promise.all(routeItem.children.map((child) => invalidateItem(`${pathname}${child.path}`, options)));
|
|
@@ -385,7 +394,6 @@ var createPrefetch = (revalidateCache) => async (pathname) => {
|
|
|
385
394
|
};
|
|
386
395
|
//#endregion
|
|
387
396
|
//#region utils/revalidateCache.ts
|
|
388
|
-
var loaderMapRef = {};
|
|
389
397
|
var loadingPromises = /* @__PURE__ */ new Map();
|
|
390
398
|
var isObjectRetry = (arg) => typeof arg === "object";
|
|
391
399
|
var createRetry = (arg) => {
|
|
@@ -406,16 +414,17 @@ var getRetry = (routeItem) => {
|
|
|
406
414
|
};
|
|
407
415
|
var sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
408
416
|
var createRevalidateCache = (routerState) => {
|
|
409
|
-
const { loaderStateRef,
|
|
417
|
+
const { loaderStateRef, contextState, loaderMap } = routerState;
|
|
410
418
|
const revalidateCache = async ({ routeItem, pathname, search = "" }, retried = 0) => {
|
|
411
419
|
if (!routeItem?.loader) return;
|
|
412
|
-
const isCacheItemFresh = createIsCacheItemFresh(
|
|
420
|
+
const isCacheItemFresh = createIsCacheItemFresh(loaderMap);
|
|
413
421
|
if (loadingPromises.has(pathname)) return loadingPromises.get(pathname);
|
|
414
422
|
if (isCacheItemFresh({
|
|
415
423
|
routeItem,
|
|
416
424
|
pathname
|
|
417
425
|
})) {
|
|
418
|
-
|
|
426
|
+
const item = loaderMap.get(pathname);
|
|
427
|
+
if (item?.state) loaderStateRef.set(item.state);
|
|
419
428
|
return;
|
|
420
429
|
}
|
|
421
430
|
const promise = (async () => {
|
|
@@ -431,13 +440,15 @@ var createRevalidateCache = (routerState) => {
|
|
|
431
440
|
setContext,
|
|
432
441
|
searchParams
|
|
433
442
|
});
|
|
434
|
-
timestampMap.set(`${pathname}${search}`, Date.now());
|
|
435
443
|
loaderStateRef.set((prev) => ({
|
|
436
444
|
...prev,
|
|
437
445
|
data: result,
|
|
438
446
|
loaderError: null
|
|
439
447
|
}));
|
|
440
|
-
|
|
448
|
+
loaderMap.set(`${pathname}${search}`, {
|
|
449
|
+
state: loaderStateRef.value,
|
|
450
|
+
timestamp: Date.now()
|
|
451
|
+
});
|
|
441
452
|
return {
|
|
442
453
|
data: result,
|
|
443
454
|
error: null
|
|
@@ -506,7 +517,7 @@ var createRouterInstance = () => {
|
|
|
506
517
|
to: ""
|
|
507
518
|
}),
|
|
508
519
|
loaderStateRef: new Cell(emptyLoaderState),
|
|
509
|
-
|
|
520
|
+
loaderMap: /* @__PURE__ */ new Map()
|
|
510
521
|
};
|
|
511
522
|
const revalidateCache = createRevalidateCache(routerState);
|
|
512
523
|
const invalidate = createInvalidate(routerState, revalidateCache);
|
|
@@ -745,7 +756,7 @@ var useLocation = () => {
|
|
|
745
756
|
};
|
|
746
757
|
//#endregion
|
|
747
758
|
//#region components/Link.tsx
|
|
748
|
-
var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, className, style,
|
|
759
|
+
var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, className, style, beforeNavigate, activeClassName = "active-link", pendingClassName = "pending-link" }) => {
|
|
749
760
|
const isPending = useIsRoutePending(to);
|
|
750
761
|
const { pathname } = useLocation();
|
|
751
762
|
const navigate = useNavigate();
|
|
@@ -804,7 +815,7 @@ var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, classNam
|
|
|
804
815
|
].filter(Boolean).join(" ");
|
|
805
816
|
const clickHandler = async (event) => {
|
|
806
817
|
event.preventDefault();
|
|
807
|
-
|
|
818
|
+
await beforeNavigate?.();
|
|
808
819
|
await navigate(to);
|
|
809
820
|
};
|
|
810
821
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { type InvalidateOptions, InvalidateResult, RevalidateCache, RouterState } from '../types';
|
|
2
|
-
export declare const createInvalidate: ({ routeItemDataState, loaderStateRef,
|
|
2
|
+
export declare const createInvalidate: ({ routeItemDataState, loaderStateRef, loaderMap, currentLoaderState, contextState }: RouterState, revalidateCache: RevalidateCache) => (pathList?: string | string[], options?: InvalidateOptions) => Promise<InvalidateResult[]>;
|
package/dist/types.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export type ClientRouteItem = {
|
|
|
25
25
|
fallback?: RenderElement;
|
|
26
26
|
children?: ClientRouteItem[];
|
|
27
27
|
staleTime?: number;
|
|
28
|
+
optimistic?: boolean;
|
|
28
29
|
pollingInterval?: number;
|
|
29
30
|
retry?: Retry;
|
|
30
31
|
preserveScroll?: boolean;
|
|
@@ -94,6 +95,10 @@ export type RouterProps = {
|
|
|
94
95
|
afterLoad?: ClientRouteItem['afterLoad'];
|
|
95
96
|
context?: Record<string, unknown>;
|
|
96
97
|
};
|
|
98
|
+
export type LoaderStateItem = {
|
|
99
|
+
state: LoaderState;
|
|
100
|
+
timestamp: number;
|
|
101
|
+
};
|
|
97
102
|
export type RouterState = {
|
|
98
103
|
routeItemDataState: Store<RouteItemData>;
|
|
99
104
|
pendingState: Store<RouteItemData | undefined>;
|
|
@@ -105,7 +110,7 @@ export type RouterState = {
|
|
|
105
110
|
to: string;
|
|
106
111
|
}>;
|
|
107
112
|
loaderStateRef: Cell<LoaderState>;
|
|
108
|
-
|
|
113
|
+
loaderMap: Map<string, LoaderStateItem>;
|
|
109
114
|
};
|
|
110
115
|
export type RouterType = {
|
|
111
116
|
state: Omit<RouterState, 'timestampMap'>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
export declare const createIsCacheItemFresh: (
|
|
1
|
+
import { LoaderStateItem, RouteItem } from '../types';
|
|
2
|
+
export declare const createIsCacheItemFresh: (loaderMap: Map<string, LoaderStateItem>) => ({ routeItem, pathname }: {
|
|
3
3
|
routeItem?: RouteItem;
|
|
4
4
|
pathname: string;
|
|
5
5
|
}) => boolean;
|