clear-react-router 1.8.4 → 1.8.6
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 +41 -6
- package/dist/components/Link.d.ts +13 -7
- package/dist/hooks/useInvalidate.d.ts +1 -1
- package/dist/hooks/useIsRoutePending.d.ts +1 -0
- package/dist/index.js +75 -27
- package/dist/runtime/invalidate.d.ts +2 -2
- package/dist/types.d.ts +12 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ It provides first-class support for:
|
|
|
70
70
|
|
|
71
71
|
### `createRouter(routes)`
|
|
72
72
|
|
|
73
|
-
Normalizes route configuration
|
|
73
|
+
Normalizes route configuration. Extracts dynamic params, builds nested paths.
|
|
74
74
|
|
|
75
75
|
| Property | Type | Description |
|
|
76
76
|
|----------|------|-------------|
|
|
@@ -88,14 +88,26 @@ Normalizes route configuration, extracts dynamic params, builds nested paths.
|
|
|
88
88
|
|
|
89
89
|
### `Link`
|
|
90
90
|
|
|
91
|
-
Component for client-side navigation with prefetch support.
|
|
91
|
+
Component for client-side navigation with prefetch support, active state detection, and pending state styling.
|
|
92
92
|
|
|
93
|
-
| Prop | Type | Default |
|
|
94
|
-
|
|
95
|
-
| `to` | `string` | required |
|
|
93
|
+
| Prop | Type | Default | Description |
|
|
94
|
+
|------|------|---------|-------------|
|
|
95
|
+
| `to` | `string` | required | Target path |
|
|
96
96
|
| `prefetch` | `'hover' \| 'render' \| 'viewport' \| 'none'` | `Router` config | Override the global prefetch strategy |
|
|
97
97
|
| `hoverPrefetchDelay` | `number` | `Router` config | Override the global hover delay |
|
|
98
|
-
| `children` | `
|
|
98
|
+
| `children` | `ReactNode` | required | Content to render inside the link |
|
|
99
|
+
| `className` | `string \| ({ isActive, isPending }) => string` | `undefined` | CSS class name(s). Can be a function for dynamic styling |
|
|
100
|
+
| `style` | `CSSProperties \| ({ isActive, isPending }) => CSSProperties` | `undefined` | Inline styles. Can be a function for dynamic styling |
|
|
101
|
+
| `activeClassName` | `string` optional | `'active-link'` | Class name applied when the link matches the current URL |
|
|
102
|
+
| `pendingClassName` | `string` optional | `'pending-link'` | Class name applied when the link's target is loading |
|
|
103
|
+
| `onClick` | `() => void` | `undefined` | Callback fired before navigation |
|
|
104
|
+
|
|
105
|
+
**State values:**
|
|
106
|
+
|
|
107
|
+
| State | Type | Description |
|
|
108
|
+
|-------|------|-------------|
|
|
109
|
+
| `isActive` | `boolean` | `true` when the link's `to` matches the current URL |
|
|
110
|
+
| `isPending` | `boolean` | `true` when the target route is currently loading (loader is running) |
|
|
99
111
|
|
|
100
112
|
### Prefetch Strategies
|
|
101
113
|
|
|
@@ -123,6 +135,21 @@ import { Router, Link } from 'clear-react-router';
|
|
|
123
135
|
<Link to="/admin" prefetch="none">
|
|
124
136
|
Admin Panel
|
|
125
137
|
</Link>
|
|
138
|
+
|
|
139
|
+
// With custom active/pending classes
|
|
140
|
+
<Link to="/settings" activeClassName="active-nav-link" pendingClassName="loading-nav-link">
|
|
141
|
+
Settings
|
|
142
|
+
</Link>
|
|
143
|
+
|
|
144
|
+
// With dynamic className
|
|
145
|
+
<Link to="/dashboard" className={({ isActive, isPending }) => isActive ? 'text-blue-600' : isPending ? 'text-gray-400' : 'text-gray-600'}>
|
|
146
|
+
Dashboard
|
|
147
|
+
</Link>
|
|
148
|
+
|
|
149
|
+
// With dynamic style
|
|
150
|
+
<Link to="/profile" style={({ isActive }) => ({ fontWeight: isActive ? 'bold' : 'normal' })}>
|
|
151
|
+
Profile
|
|
152
|
+
</Link>
|
|
126
153
|
```
|
|
127
154
|
**Important**: prefetch="render" should be used sparingly, as it preloads data immediately when the link is rendered, which may cause unnecessary network requests.
|
|
128
155
|
|
|
@@ -557,6 +584,14 @@ await invalidate('/posts', { withBeforeLoad: true });
|
|
|
557
584
|
await invalidate(['/posts', '/users'], { withBeforeLoad: true });
|
|
558
585
|
```
|
|
559
586
|
|
|
587
|
+
#### Returns
|
|
588
|
+
|
|
589
|
+
An array of objects with the following structure:
|
|
590
|
+
```ts
|
|
591
|
+
{ path: string; data: unknown; error: unknown }
|
|
592
|
+
```
|
|
593
|
+
Each object represents a revalidated route, where `path` is the route pathname, `data` is the revalidated loader result, and `error` is the loader error, if any.
|
|
594
|
+
|
|
560
595
|
#### Notes
|
|
561
596
|
|
|
562
597
|
* **Only routes that already have cached data are revalidated.**
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type CSSProperties, ReactNode, ComponentPropsWithoutRef } from 'react';
|
|
2
2
|
import { RouterProps } from '../types';
|
|
3
|
-
type
|
|
3
|
+
type States = {
|
|
4
|
+
isActive: boolean;
|
|
5
|
+
isPending: boolean;
|
|
6
|
+
};
|
|
7
|
+
type LinkProps = Omit<ComponentPropsWithoutRef<'a'>, 'href' | 'className' | 'style'> & {
|
|
4
8
|
to: string;
|
|
5
|
-
children:
|
|
6
|
-
onClick: (e: MouseEvent) => void;
|
|
7
|
-
style: CSSProperties;
|
|
8
|
-
}>;
|
|
9
|
+
children: ReactNode;
|
|
9
10
|
prefetch?: RouterProps['prefetch'];
|
|
10
11
|
hoverPrefetchDelay?: number;
|
|
12
|
+
style?: CSSProperties | ((arg: States) => CSSProperties);
|
|
13
|
+
className?: string | ((arg: States) => string);
|
|
14
|
+
activeClassName?: string;
|
|
15
|
+
pendingClassName?: string;
|
|
16
|
+
onClick?(): void;
|
|
11
17
|
};
|
|
12
|
-
export declare const Link: ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }: LinkProps) => import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export declare const Link: ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, className, style, onClick, activeClassName, pendingClassName, }: LinkProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
19
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const useInvalidate: () => (pathList?: string | string[], options?: import("../types").InvalidateOptions) => Promise<
|
|
1
|
+
export declare const useInvalidate: () => (pathList?: string | string[], options?: import("../types").InvalidateOptions) => Promise<import("../types").InvalidateResult[]>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useIsRoutePending: (routePath: string) => boolean;
|
package/dist/index.js
CHANGED
|
@@ -212,7 +212,7 @@ var findRoute = (pathname, includeAll) => {
|
|
|
212
212
|
//#region runtime/navigate.ts
|
|
213
213
|
var navigationSeq = 0;
|
|
214
214
|
var createNavigate = (routerState, revalidateCache) => {
|
|
215
|
-
const { loaderStateRef, scrollMapState, prevPathnameRef, loaderFallbackState, isLoadingState, contextState, timestampMap } = routerState;
|
|
215
|
+
const { loaderStateRef, scrollMapState, prevPathnameRef, loaderFallbackState, isLoadingState, contextState, timestampMap, pendingPathRef } = routerState;
|
|
216
216
|
const commitNavigation = createCommitNavigation(createCommitState(routerState), prevPathnameRef);
|
|
217
217
|
const isCacheItemFresh = createIsCacheItemFresh(timestampMap);
|
|
218
218
|
const getContext = () => ({
|
|
@@ -269,10 +269,12 @@ var createNavigate = (routerState, revalidateCache) => {
|
|
|
269
269
|
const loader = async (routeItem, location) => {
|
|
270
270
|
if (!routeItem?.loader) return;
|
|
271
271
|
isLoadingState.setState(true);
|
|
272
|
+
pendingPathRef.set(location.pathname);
|
|
272
273
|
await revalidateCache({
|
|
273
274
|
routeItem,
|
|
274
275
|
pathname: location.pathname
|
|
275
276
|
});
|
|
277
|
+
pendingPathRef.set("");
|
|
276
278
|
};
|
|
277
279
|
const afterLoad = async (routeItem, params) => {
|
|
278
280
|
const { afterLoad } = routerConfig;
|
|
@@ -328,27 +330,30 @@ var createInvalidate = ({ routeItemDataState, loaderStateRef, timestampMap, curr
|
|
|
328
330
|
beforeLoadError: error
|
|
329
331
|
}));
|
|
330
332
|
}
|
|
331
|
-
await revalidateCache({
|
|
333
|
+
const result = await revalidateCache({
|
|
332
334
|
routeItem,
|
|
333
335
|
pathname
|
|
334
336
|
});
|
|
335
337
|
if (pathname === routePathname) currentLoaderState.setState(loaderStateRef.value);
|
|
338
|
+
return {
|
|
339
|
+
path: pathname,
|
|
340
|
+
...result
|
|
341
|
+
};
|
|
336
342
|
};
|
|
337
343
|
const invalidateItem = async (pathname, options) => {
|
|
338
344
|
const routeItem = findRoute(pathname);
|
|
339
|
-
if (!routeItem) return;
|
|
345
|
+
if (!routeItem) return [];
|
|
340
346
|
const pathnameArray = [];
|
|
341
347
|
for (const [key] of timestampMap) if (comparePaths(routeItem, key)) pathnameArray.push(key);
|
|
342
|
-
await Promise.all(pathnameArray.map((pathname) => invalidatePath(routeItem, pathname, options)));
|
|
343
|
-
if (options?.withChildren
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
348
|
+
const currentResults = await Promise.all(pathnameArray.map((pathname) => invalidatePath(routeItem, pathname, options)));
|
|
349
|
+
if (!options?.withChildren || !routeItem.children?.length) return currentResults;
|
|
350
|
+
const childResults = await Promise.all(routeItem.children.map((child) => invalidateItem(`${pathname}${child.path}`, options)));
|
|
351
|
+
return [...currentResults, ...childResults.flat()];
|
|
347
352
|
};
|
|
348
353
|
return async (pathList, options) => {
|
|
349
354
|
const routePathname = routeItemDataState.getState().location.pathname;
|
|
350
355
|
const pathnameList = Array.isArray(pathList) ? pathList : pathList ? [pathList] : [routePathname];
|
|
351
|
-
await Promise.all(pathnameList.map((pathname) => invalidateItem(pathname, options)));
|
|
356
|
+
return (await Promise.all(pathnameList.map((pathname) => invalidateItem(pathname, options)))).flat();
|
|
352
357
|
};
|
|
353
358
|
};
|
|
354
359
|
//#endregion
|
|
@@ -383,18 +388,15 @@ var getRetry = (routeItem) => {
|
|
|
383
388
|
};
|
|
384
389
|
var sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
385
390
|
var createRevalidateCache = (routerState) => {
|
|
391
|
+
const { loaderStateRef, timestampMap, contextState } = routerState;
|
|
386
392
|
const revalidateCache = async ({ routeItem, pathname }, retried = 0) => {
|
|
387
393
|
if (!routeItem?.loader) return;
|
|
388
|
-
const isCacheItemFresh = createIsCacheItemFresh(
|
|
389
|
-
const { loaderStateRef, timestampMap, contextState } = routerState;
|
|
394
|
+
const isCacheItemFresh = createIsCacheItemFresh(timestampMap);
|
|
390
395
|
if (loadingPromises.has(pathname)) return loadingPromises.get(pathname);
|
|
391
396
|
if (isCacheItemFresh({
|
|
392
397
|
routeItem,
|
|
393
398
|
pathname
|
|
394
|
-
}))
|
|
395
|
-
loaderStateRef.set(loaderMapRef[pathname]);
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
399
|
+
})) loaderStateRef.set(loaderMapRef[pathname]);
|
|
398
400
|
const promise = (async () => {
|
|
399
401
|
if (!routeItem?.loader) return;
|
|
400
402
|
try {
|
|
@@ -413,6 +415,10 @@ var createRevalidateCache = (routerState) => {
|
|
|
413
415
|
loaderError: null
|
|
414
416
|
}));
|
|
415
417
|
loaderMapRef[pathname] = loaderStateRef.value;
|
|
418
|
+
return {
|
|
419
|
+
data: result,
|
|
420
|
+
error: null
|
|
421
|
+
};
|
|
416
422
|
} catch (error) {
|
|
417
423
|
const retry = getRetry(routeItem);
|
|
418
424
|
if (retry && retry.count > retried) {
|
|
@@ -422,11 +428,21 @@ var createRevalidateCache = (routerState) => {
|
|
|
422
428
|
routeItem,
|
|
423
429
|
pathname
|
|
424
430
|
}, retried + 1);
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
431
|
+
return {
|
|
432
|
+
data: null,
|
|
433
|
+
error
|
|
434
|
+
};
|
|
435
|
+
} else {
|
|
436
|
+
loaderStateRef.set((prev) => ({
|
|
437
|
+
...prev,
|
|
438
|
+
data: null,
|
|
439
|
+
loaderError: error
|
|
440
|
+
}));
|
|
441
|
+
return {
|
|
442
|
+
data: null,
|
|
443
|
+
error
|
|
444
|
+
};
|
|
445
|
+
}
|
|
430
446
|
} finally {
|
|
431
447
|
loadingPromises.delete(pathname);
|
|
432
448
|
}
|
|
@@ -471,6 +487,7 @@ var createRouterInstance = () => {
|
|
|
471
487
|
}),
|
|
472
488
|
loaderStateRef: new Cell(emptyLoaderState),
|
|
473
489
|
prevPathnameRef: new Cell(""),
|
|
490
|
+
pendingPathRef: new Cell(""),
|
|
474
491
|
timestampMap: /* @__PURE__ */ new Map()
|
|
475
492
|
};
|
|
476
493
|
const revalidateCache = createRevalidateCache(routerState);
|
|
@@ -505,7 +522,8 @@ var createRouterInstance = () => {
|
|
|
505
522
|
scrollMapState: routerState.scrollMapState,
|
|
506
523
|
contextState: routerState.contextState,
|
|
507
524
|
blockedRouteState: routerState.blockedRouteState,
|
|
508
|
-
prevPathnameRef: routerState.prevPathnameRef
|
|
525
|
+
prevPathnameRef: routerState.prevPathnameRef,
|
|
526
|
+
pendingPathRef: routerState.pendingPathRef
|
|
509
527
|
},
|
|
510
528
|
runtime: {
|
|
511
529
|
navigate,
|
|
@@ -711,17 +729,26 @@ var Router = ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated = f
|
|
|
711
729
|
});
|
|
712
730
|
};
|
|
713
731
|
//#endregion
|
|
732
|
+
//#region hooks/useIsRoutePending.ts
|
|
733
|
+
var useIsRoutePending = (routePath) => {
|
|
734
|
+
const { hooks: { useIsLoading }, state: { pendingPathRef } } = router;
|
|
735
|
+
const [isPending] = useIsLoading();
|
|
736
|
+
return isPending && pendingPathRef.value === routePath;
|
|
737
|
+
};
|
|
738
|
+
//#endregion
|
|
714
739
|
//#region hooks/useNavigate.ts
|
|
715
740
|
var useNavigate = router.hooks.useNavigate;
|
|
716
741
|
//#endregion
|
|
717
742
|
//#region components/Link.tsx
|
|
718
|
-
var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }) => {
|
|
719
|
-
const
|
|
720
|
-
const
|
|
721
|
-
const prefetchDelay = hoverPrefetchDelay ?? configPrefetchDelay;
|
|
743
|
+
var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, className, style, onClick, activeClassName = "active-link", pendingClassName = "pending-link" }) => {
|
|
744
|
+
const isPending = useIsRoutePending(to);
|
|
745
|
+
const { pathname } = useLocation();
|
|
722
746
|
const navigate = useNavigate();
|
|
723
747
|
const timeout = useRef(0);
|
|
724
748
|
const ref = useRef(null);
|
|
749
|
+
const { prefetch: configPrefetch, hoverPrefetchDelay: configPrefetchDelay } = routerConfig;
|
|
750
|
+
const prefetch = prefetchLink || configPrefetch;
|
|
751
|
+
const prefetchDelay = hoverPrefetchDelay ?? configPrefetchDelay;
|
|
725
752
|
const onMouseEnter = useCallback(() => {
|
|
726
753
|
if (prefetch !== "hover" || !prefetchDelay) return;
|
|
727
754
|
if (timeout.current) clearTimeout(timeout.current);
|
|
@@ -756,10 +783,31 @@ var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }) => {
|
|
|
756
783
|
if (element) observer.disconnect();
|
|
757
784
|
};
|
|
758
785
|
}, [prefetch, to]);
|
|
786
|
+
const isActive = to === pathname;
|
|
787
|
+
const normalizedClassName = typeof className === "function" ? className({
|
|
788
|
+
isActive,
|
|
789
|
+
isPending
|
|
790
|
+
}) : className;
|
|
791
|
+
const normalizedStyle = typeof style === "function" ? style({
|
|
792
|
+
isActive,
|
|
793
|
+
isPending
|
|
794
|
+
}) : style;
|
|
795
|
+
const resultClassName = [
|
|
796
|
+
isActive && activeClassName,
|
|
797
|
+
isPending && pendingClassName,
|
|
798
|
+
normalizedClassName
|
|
799
|
+
].filter(Boolean).join(" ");
|
|
800
|
+
const clickHandler = async (event) => {
|
|
801
|
+
event.preventDefault();
|
|
802
|
+
onClick?.();
|
|
803
|
+
await navigate(to);
|
|
804
|
+
};
|
|
759
805
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
|
|
806
|
+
href: to,
|
|
760
807
|
ref,
|
|
761
|
-
style:
|
|
762
|
-
|
|
808
|
+
style: normalizedStyle,
|
|
809
|
+
className: resultClassName,
|
|
810
|
+
onClick: clickHandler,
|
|
763
811
|
onMouseEnter,
|
|
764
812
|
onMouseLeave,
|
|
765
813
|
children
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { type InvalidateOptions, RevalidateCache, RouterState } from '../types';
|
|
2
|
-
export declare const createInvalidate: ({ routeItemDataState, loaderStateRef, timestampMap, currentLoaderState, contextState }: RouterState, revalidateCache: RevalidateCache) => (pathList?: string | string[], options?: InvalidateOptions) => Promise<
|
|
1
|
+
import { type InvalidateOptions, InvalidateResult, RevalidateCache, RouterState } from '../types';
|
|
2
|
+
export declare const createInvalidate: ({ routeItemDataState, loaderStateRef, timestampMap, currentLoaderState, contextState }: RouterState, revalidateCache: RevalidateCache) => (pathList?: string | string[], options?: InvalidateOptions) => Promise<InvalidateResult[]>;
|
package/dist/types.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type ClientRouteItem = {
|
|
|
34
34
|
actions?: (arg: {
|
|
35
35
|
context: Record<string, unknown>;
|
|
36
36
|
params: Record<string, string>;
|
|
37
|
-
invalidate: (path?: string) => Promise<
|
|
37
|
+
invalidate: (path?: string) => Promise<InvalidateResult[]>;
|
|
38
38
|
setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
|
|
39
39
|
}) => Record<string, (arg: FormData) => Promise<unknown> | Promise<void> | void | unknown>;
|
|
40
40
|
};
|
|
@@ -103,13 +103,14 @@ export type RouterState = {
|
|
|
103
103
|
}>;
|
|
104
104
|
loaderStateRef: Cell<LoaderState>;
|
|
105
105
|
prevPathnameRef: Cell<string>;
|
|
106
|
+
pendingPathRef: Cell<string>;
|
|
106
107
|
timestampMap: Map<string, number>;
|
|
107
108
|
};
|
|
108
109
|
export type RouterType = {
|
|
109
110
|
state: Omit<RouterState, 'loaderStateRef' | 'timestampMap'>;
|
|
110
111
|
runtime: {
|
|
111
112
|
navigate(arg: Location): Promise<void>;
|
|
112
|
-
invalidate(pathList?: string | string[], options?: InvalidateOptions): Promise<
|
|
113
|
+
invalidate(pathList?: string | string[], options?: InvalidateOptions): Promise<InvalidateResult[]>;
|
|
113
114
|
prefetch(pathname: string): Promise<void>;
|
|
114
115
|
};
|
|
115
116
|
hooks: {
|
|
@@ -127,7 +128,7 @@ export type RouterType = {
|
|
|
127
128
|
useNavigate: () => (arg: Location | string | -1) => Promise<void>;
|
|
128
129
|
useGetAction: (actionKey: string) => {
|
|
129
130
|
currentAction: (arg: FormData) => Promise<unknown> | Promise<void> | void | unknown;
|
|
130
|
-
invalidate: (pathList?: string | string[], options?: InvalidateOptions) => Promise<
|
|
131
|
+
invalidate: (pathList?: string | string[], options?: InvalidateOptions) => Promise<InvalidateResult[]>;
|
|
131
132
|
};
|
|
132
133
|
useRestoreScroll: () => () => void;
|
|
133
134
|
useAction: (action: string, options?: Options) => (arg: FormData) => Promise<void>;
|
|
@@ -137,9 +138,16 @@ export type InvalidateOptions = {
|
|
|
137
138
|
withChildren?: boolean;
|
|
138
139
|
withBeforeLoad?: boolean;
|
|
139
140
|
};
|
|
140
|
-
export type RevalidateCache = ({ routeItem, pathname }: RevalidateCacheArgs) => Promise<
|
|
141
|
+
export type RevalidateCache = ({ routeItem, pathname, }: RevalidateCacheArgs) => Promise<{
|
|
142
|
+
data: unknown;
|
|
143
|
+
error: unknown;
|
|
144
|
+
}>;
|
|
141
145
|
export type Options = Partial<{
|
|
142
146
|
onSuccess: (args: unknown) => void;
|
|
143
147
|
onError: (args: unknown) => void;
|
|
144
148
|
}> | undefined;
|
|
149
|
+
export type InvalidateResult = {
|
|
150
|
+
path: string;
|
|
151
|
+
data: unknown;
|
|
152
|
+
};
|
|
145
153
|
export {};
|