clear-react-router 1.5.7 → 1.6.0
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 +31 -9
- package/dist/components/Link.d.ts +4 -3
- package/dist/components/Router.d.ts +2 -9
- package/dist/config/routerConfig.d.ts +3 -0
- package/dist/constants.d.ts +3 -0
- package/dist/context/RouterProviderContext.d.ts +2 -2
- package/dist/hooks/useHandleNavigation.d.ts +7 -6
- package/dist/hooks/useLoader.d.ts +3 -4
- package/dist/hooks/useSearch.d.ts +1 -0
- package/dist/hooks/useSetRouterConfig.d.ts +2 -0
- package/dist/index.js +189 -124
- package/dist/provider/Provider.d.ts +1 -1
- package/dist/types/global.d.ts +10 -3
- package/package.json +1 -1
- package/dist/hooks/useSetIsAnimated.d.ts +0 -1
package/README.md
CHANGED
|
@@ -72,6 +72,8 @@ Renders the current route's component. Must be placed inside `<RouterProvider>`.
|
|
|
72
72
|
| `spinner` | `boolean \| undefined` | `true` | Show a small spinner in the corner while loading data (only when `isAnimated` is enabled) |
|
|
73
73
|
| `preserveScroll` | `boolean \| undefined` | `true` | Save and restore scroll position when navigating between pages |
|
|
74
74
|
| `showFallbackIfAnimated` | `boolean \| undefined` | `false` | Show `loaderFallback` even when `isAnimated` is `true` (instead of spinner) |
|
|
75
|
+
| `prefetch` | `'hover' \| 'render' \| 'viewport' \| 'none'` | `'hover'` | Default prefetch strategy for all `<Link>` components |
|
|
76
|
+
| `hoverPrefetchDelay` | `number` | `150` | Delay in milliseconds before prefetching on hover (only for `'hover'` strategy) |
|
|
75
77
|
|
|
76
78
|
```
|
|
77
79
|
<RouterProvider routeList={routes} isAnimated>
|
|
@@ -89,20 +91,40 @@ Component for client-side navigation with prefetch support.
|
|
|
89
91
|
| Prop | Type | Default |
|
|
90
92
|
|------|------|---------|
|
|
91
93
|
| `to` | `string` | required |
|
|
92
|
-
| `prefetch` | `
|
|
93
|
-
| `
|
|
94
|
-
| `children` | `ReactElement` | required |
|
|
94
|
+
| `prefetch` | `'hover' \| 'render' \| 'viewport' \| 'none'` | `Router` config | Override the global prefetch strategy |
|
|
95
|
+
| `hoverPrefetchDelay` | `number` | `Router` config | Override the global hover delay |
|
|
96
|
+
| `children` | `ReactElement` | required | Content to render inside the link |
|
|
97
|
+
|
|
98
|
+
### Prefetch Strategies
|
|
99
|
+
|
|
100
|
+
| Strategy | Behavior |
|
|
101
|
+
|----------|----------|
|
|
102
|
+
| `'hover'` | Prefetches when the user hovers over the link (with configurable delay) |
|
|
103
|
+
| `'render'` | Prefetches immediately when the link is rendered |
|
|
104
|
+
| `'viewport'` | Prefetches when the link enters the viewport (using Intersection Observer) |
|
|
105
|
+
| `'none'` | No prefetching |
|
|
106
|
+
|
|
107
|
+
**Example:**
|
|
95
108
|
|
|
96
109
|
```
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
import { RouterProvider, Router, Link } from 'clear-react-router';
|
|
111
|
+
|
|
112
|
+
// Global prefetch: hover with 100ms delay
|
|
113
|
+
<RouterProvider routeList={routes} prefetch="hover" hoverPrefetchDelay={100}>
|
|
114
|
+
<Router />
|
|
115
|
+
</RouterProvider>
|
|
99
116
|
|
|
100
|
-
//
|
|
101
|
-
<Link to="/heavy-page"
|
|
117
|
+
// Override for a specific link
|
|
118
|
+
<Link to="/heavy-page" prefetch="viewport">
|
|
119
|
+
Heavy Page
|
|
120
|
+
</Link>
|
|
102
121
|
|
|
103
|
-
// Disable prefetch
|
|
104
|
-
<Link to="/
|
|
122
|
+
// Disable prefetch for a specific link
|
|
123
|
+
<Link to="/admin" prefetch="none">
|
|
124
|
+
Admin Panel
|
|
125
|
+
</Link>
|
|
105
126
|
```
|
|
127
|
+
**Important**: prefetch="render" should be used sparingly, as it preloads data immediately when the link is rendered, which may cause unnecessary network requests.
|
|
106
128
|
|
|
107
129
|
### `redirect`
|
|
108
130
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { type ReactElement, type MouseEvent, type CSSProperties } from 'react';
|
|
2
|
+
import { RouterProps } from '../types/global';
|
|
2
3
|
type LinkProps = {
|
|
3
4
|
to: string;
|
|
4
5
|
children: ReactElement<{
|
|
5
6
|
onClick: (e: MouseEvent) => void;
|
|
6
7
|
style: CSSProperties;
|
|
7
8
|
}>;
|
|
8
|
-
prefetch?:
|
|
9
|
-
|
|
9
|
+
prefetch?: RouterProps['prefetch'];
|
|
10
|
+
hoverPrefetchDelay?: number;
|
|
10
11
|
};
|
|
11
|
-
export declare const Link: ({ children, to, prefetch,
|
|
12
|
+
export declare const Link: ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }: LinkProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
13
|
export {};
|
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
animationDuration?: number;
|
|
4
|
-
spinner?: boolean;
|
|
5
|
-
preserveScroll?: boolean;
|
|
6
|
-
showFallbackIfAnimated?: boolean;
|
|
7
|
-
};
|
|
8
|
-
export declare const Router: ({ isAnimated, animationDuration, spinner, preserveScroll, showFallbackIfAnimated, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
9
|
-
export {};
|
|
1
|
+
import { RouterProps } from '../types/global';
|
|
2
|
+
export declare const Router: ({ isAnimated, animationDuration, spinner, preserveScroll, showFallbackIfAnimated, prefetch, hoverPrefetchDelay, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { RouterProps } from '../types/global';
|
|
1
2
|
declare class RouterConfig {
|
|
2
3
|
isAnimated: boolean;
|
|
3
4
|
showFallbackIfAnimated: boolean;
|
|
5
|
+
prefetch: RouterProps['prefetch'];
|
|
6
|
+
hoverPrefetchDelay: number;
|
|
4
7
|
configure(config: Partial<RouterConfig>): void;
|
|
5
8
|
}
|
|
6
9
|
export declare const routerConfig: RouterConfig;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { BlockerState, Location, RouteItem, RouteItemData, UpdateBlockedRouteProps } from '../types/global';
|
|
1
|
+
import { BlockerState, LoaderState, Location, RouteItem, RouteItemData, UpdateBlockedRouteProps } from '../types/global';
|
|
2
2
|
export type NavigationContextValue = {
|
|
3
3
|
blockerState: BlockerState;
|
|
4
4
|
routeItemData: RouteItemData;
|
|
5
5
|
currentLoaderFallback: RouteItem['loaderFallback'];
|
|
6
6
|
isLoading: boolean;
|
|
7
|
+
loaderState: LoaderState;
|
|
7
8
|
};
|
|
8
9
|
export type ActionsContextValue = {
|
|
9
|
-
setSearch(arg: string): void;
|
|
10
10
|
updateLocation(route: Location): Promise<void>;
|
|
11
11
|
updateBlockedRoute(arg: UpdateBlockedRouteProps): void;
|
|
12
12
|
prefetchLoader(arg: string): Promise<void>;
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import { type Dispatch, type SetStateAction } from 'react';
|
|
2
|
-
import { BlockerState, Location, RevalidateCacheArgs, RouteItem, RouteItemData, UpdateBlockedRouteProps } from '../types/global';
|
|
1
|
+
import { type Dispatch, RefObject, type SetStateAction } from 'react';
|
|
2
|
+
import { BlockerState, LoaderState, Location, RevalidateCacheArgs, RouteItem, RouteItemData, UpdateBlockedRouteProps } from '../types/global';
|
|
3
3
|
type UseHandleNavigation = {
|
|
4
4
|
routeList: RouteItem[];
|
|
5
5
|
context: Record<string, unknown>;
|
|
6
|
-
revalidateCache(arg: RevalidateCacheArgs): Promise<
|
|
6
|
+
revalidateCache(arg: RevalidateCacheArgs): Promise<unknown>;
|
|
7
7
|
setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
|
|
8
8
|
isCacheItemFresh(arg: {
|
|
9
9
|
routeItem?: RouteItem;
|
|
10
10
|
pathname: string;
|
|
11
11
|
}): boolean;
|
|
12
|
-
|
|
12
|
+
loaderStateRef: RefObject<LoaderState>;
|
|
13
13
|
};
|
|
14
|
-
export declare const useHandleNavigation: ({ routeList, context, revalidateCache, setContext, isCacheItemFresh,
|
|
14
|
+
export declare const useHandleNavigation: ({ routeList, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef, }: UseHandleNavigation) => {
|
|
15
15
|
blockerState: BlockerState;
|
|
16
16
|
updateLocation: (nextLocation: Location) => Promise<void>;
|
|
17
17
|
updateBlockedRoute: ({ type, payload }: UpdateBlockedRouteProps) => void;
|
|
18
18
|
routeItemData: RouteItemData;
|
|
19
|
-
setSearch: (search: string) => void;
|
|
20
19
|
restoreScroll: () => void;
|
|
21
20
|
currentLoaderFallback: import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | (() => import("react").ReactElement) | undefined;
|
|
21
|
+
isLoading: boolean;
|
|
22
|
+
loaderState: LoaderState;
|
|
22
23
|
};
|
|
23
24
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Dispatch, type SetStateAction } from 'react';
|
|
2
|
-
import type { RevalidateCacheArgs, RouteItem } from '../types/global';
|
|
2
|
+
import type { LoaderState, RevalidateCacheArgs, RouteItem } from '../types/global';
|
|
3
3
|
type UseLoaderParams = {
|
|
4
4
|
routeList: RouteItem[];
|
|
5
5
|
context: Record<string, unknown>;
|
|
@@ -7,12 +7,11 @@ type UseLoaderParams = {
|
|
|
7
7
|
};
|
|
8
8
|
export declare const useLoader: ({ routeList, context, setContext }: UseLoaderParams) => {
|
|
9
9
|
prefetchLoader: (pathname: string) => Promise<void>;
|
|
10
|
-
revalidateCache: ({ routeItem,
|
|
10
|
+
revalidateCache: ({ routeItem, pathname }: RevalidateCacheArgs) => Promise<unknown>;
|
|
11
11
|
isCacheItemFresh: ({ routeItem, pathname }: {
|
|
12
12
|
routeItem?: RouteItem;
|
|
13
13
|
pathname: string;
|
|
14
14
|
}) => boolean;
|
|
15
|
-
|
|
16
|
-
setIsLoading: Dispatch<SetStateAction<boolean>>;
|
|
15
|
+
loaderStateRef: import("react").RefObject<LoaderState>;
|
|
17
16
|
};
|
|
18
17
|
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useSearch: () => string;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Suspense, createContext, lazy, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { Suspense, createContext, lazy, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
2
|
//#region \0rolldown/runtime.js
|
|
3
3
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
4
4
|
//#endregion
|
|
@@ -45,10 +45,9 @@ var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((expor
|
|
|
45
45
|
var import_jsx_runtime = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
46
46
|
module.exports = require_react_jsx_runtime_production();
|
|
47
47
|
})))();
|
|
48
|
-
var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocation,
|
|
48
|
+
var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocation, prefetchLoader, blockerState, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState }) => {
|
|
49
49
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionsContext.Provider, {
|
|
50
50
|
value: {
|
|
51
|
-
setSearch,
|
|
52
51
|
updateLocation,
|
|
53
52
|
updateBlockedRoute,
|
|
54
53
|
prefetchLoader,
|
|
@@ -62,7 +61,8 @@ var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocat
|
|
|
62
61
|
blockerState,
|
|
63
62
|
routeItemData,
|
|
64
63
|
currentLoaderFallback,
|
|
65
|
-
isLoading
|
|
64
|
+
isLoading,
|
|
65
|
+
loaderState
|
|
66
66
|
},
|
|
67
67
|
children
|
|
68
68
|
})
|
|
@@ -79,6 +79,9 @@ var useLatest = (value) => {
|
|
|
79
79
|
return ref;
|
|
80
80
|
};
|
|
81
81
|
//#endregion
|
|
82
|
+
//#region constants.ts
|
|
83
|
+
var emptyLoaderState = {};
|
|
84
|
+
//#endregion
|
|
82
85
|
//#region \0@oxc-project+runtime@0.133.0/helpers/esm/typeof.js
|
|
83
86
|
function _typeof(o) {
|
|
84
87
|
"@babel/helpers - typeof";
|
|
@@ -122,6 +125,8 @@ var RouterConfig = class {
|
|
|
122
125
|
constructor() {
|
|
123
126
|
_defineProperty(this, "isAnimated", false);
|
|
124
127
|
_defineProperty(this, "showFallbackIfAnimated", false);
|
|
128
|
+
_defineProperty(this, "prefetch", "hover");
|
|
129
|
+
_defineProperty(this, "hoverPrefetchDelay", 150);
|
|
125
130
|
}
|
|
126
131
|
configure(config) {
|
|
127
132
|
Object.assign(this, config);
|
|
@@ -182,30 +187,23 @@ var comparePaths = (el, pathname) => {
|
|
|
182
187
|
//#endregion
|
|
183
188
|
//#region hooks/useHandleNavigation.ts
|
|
184
189
|
var ALL_LOCATIONS = "*";
|
|
185
|
-
var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, isCacheItemFresh,
|
|
190
|
+
var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef }) => {
|
|
186
191
|
const { isAnimated, showFallbackIfAnimated: showFallback } = routerConfig;
|
|
192
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
187
193
|
const [blockedRoute, setBlockedRoute] = useState({
|
|
188
194
|
from: "",
|
|
189
195
|
to: ""
|
|
190
196
|
});
|
|
191
197
|
const [routeItemData, setRouteItemData] = useState({
|
|
192
|
-
location: {},
|
|
193
198
|
routeItem: void 0,
|
|
194
|
-
|
|
199
|
+
location: {}
|
|
195
200
|
});
|
|
196
201
|
const [scrollMap, setScrollMap] = useState({});
|
|
197
202
|
const [currentLoaderFallback, setCurrentLoaderFallback] = useState();
|
|
198
|
-
const loaderState =
|
|
203
|
+
const [loaderState, setLoaderState] = useState(emptyLoaderState);
|
|
199
204
|
const prevPathname = useRef("");
|
|
200
205
|
const navigationSeq = useRef(0);
|
|
201
206
|
const scrollMapRef = useLatest(scrollMap);
|
|
202
|
-
const setSearch = useCallback((search) => setRouteItemData((prevState) => ({
|
|
203
|
-
...prevState,
|
|
204
|
-
location: {
|
|
205
|
-
...prevState.location,
|
|
206
|
-
search
|
|
207
|
-
}
|
|
208
|
-
})), []);
|
|
209
207
|
const restoreScroll = useCallback(() => {
|
|
210
208
|
if (!prevPathname.current || !scrollMapRef.current[prevPathname.current]) return;
|
|
211
209
|
requestAnimationFrame(() => {
|
|
@@ -217,17 +215,17 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
|
|
|
217
215
|
}, [scrollMapRef]);
|
|
218
216
|
const navigation = useCallback((nextLocation, routeItem) => {
|
|
219
217
|
setRouteItemData({
|
|
220
|
-
location: nextLocation,
|
|
221
218
|
routeItem,
|
|
222
|
-
|
|
219
|
+
location: nextLocation
|
|
223
220
|
});
|
|
221
|
+
setLoaderState(loaderStateRef.current);
|
|
224
222
|
setIsLoading(false);
|
|
225
223
|
setCurrentLoaderFallback(void 0);
|
|
226
224
|
prevPathname.current = nextLocation.pathname;
|
|
227
225
|
const fullPath = nextLocation.search ? `${nextLocation.pathname}${nextLocation.search}` : nextLocation.pathname;
|
|
228
226
|
if (fullPath === window.location.pathname + window.location.search) return;
|
|
229
227
|
history.pushState(null, "", fullPath);
|
|
230
|
-
}, []);
|
|
228
|
+
}, [loaderStateRef]);
|
|
231
229
|
const transitionedNavigation = useCallback((nextLocation, routeItem) => {
|
|
232
230
|
if (!isAnimated) {
|
|
233
231
|
navigation(nextLocation, routeItem);
|
|
@@ -242,7 +240,7 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
|
|
|
242
240
|
const navigationHandler = useCallback(async (nextLocation) => {
|
|
243
241
|
navigationSeq.current = navigationSeq.current + 1;
|
|
244
242
|
const seq = navigationSeq.current;
|
|
245
|
-
|
|
243
|
+
loaderStateRef.current = emptyLoaderState;
|
|
246
244
|
const nextItem = routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, nextLocation.pathname));
|
|
247
245
|
const params = getParamsObject({
|
|
248
246
|
params: nextItem?.params,
|
|
@@ -256,13 +254,13 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
|
|
|
256
254
|
params,
|
|
257
255
|
setContext
|
|
258
256
|
});
|
|
259
|
-
|
|
260
|
-
...
|
|
257
|
+
loaderStateRef.current = {
|
|
258
|
+
...loaderStateRef.current,
|
|
261
259
|
beforeLoadError: null
|
|
262
260
|
};
|
|
263
261
|
} catch (error) {
|
|
264
|
-
|
|
265
|
-
...
|
|
262
|
+
loaderStateRef.current = {
|
|
263
|
+
...loaderStateRef.current,
|
|
266
264
|
beforeLoadError: error
|
|
267
265
|
};
|
|
268
266
|
return transitionedNavigation(nextLocation, nextItem);
|
|
@@ -280,11 +278,13 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
|
|
|
280
278
|
routeItem: nextItem,
|
|
281
279
|
pathname: nextLocation.pathname
|
|
282
280
|
}) || isAnimated && !showFallback ? void 0 : nextItem?.loaderFallback);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
281
|
+
if (nextItem?.loader) {
|
|
282
|
+
setIsLoading(true);
|
|
283
|
+
await revalidateCache({
|
|
284
|
+
routeItem: nextItem,
|
|
285
|
+
pathname: nextLocation.pathname
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
288
|
if (seq !== navigationSeq.current) return;
|
|
289
289
|
transitionedNavigation(nextLocation, nextItem);
|
|
290
290
|
if (nextItem?.afterLoad) await nextItem.afterLoad({
|
|
@@ -300,7 +300,8 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
|
|
|
300
300
|
setContext,
|
|
301
301
|
isCacheItemFresh,
|
|
302
302
|
isAnimated,
|
|
303
|
-
showFallback
|
|
303
|
+
showFallback,
|
|
304
|
+
loaderStateRef
|
|
304
305
|
]);
|
|
305
306
|
const setNextLocationRef = useLatest(navigationHandler);
|
|
306
307
|
const updateBlockedRoute = useCallback(({ type, payload = "" }) => setBlockedRoute((prevState) => {
|
|
@@ -355,69 +356,74 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
|
|
|
355
356
|
updateLocation,
|
|
356
357
|
updateBlockedRoute,
|
|
357
358
|
routeItemData,
|
|
358
|
-
setSearch,
|
|
359
359
|
restoreScroll,
|
|
360
|
-
currentLoaderFallback
|
|
360
|
+
currentLoaderFallback,
|
|
361
|
+
isLoading,
|
|
362
|
+
loaderState
|
|
361
363
|
};
|
|
362
364
|
};
|
|
363
365
|
//#endregion
|
|
364
366
|
//#region hooks/useLoader.ts
|
|
365
367
|
var useLoader = ({ routeList, context, setContext }) => {
|
|
366
|
-
const
|
|
367
|
-
const
|
|
368
|
-
const
|
|
368
|
+
const timestampMapRef = useRef({});
|
|
369
|
+
const loaderMapRef = useRef({});
|
|
370
|
+
const loaderStateRef = useRef(emptyLoaderState);
|
|
371
|
+
const loadingPromises = useRef(/* @__PURE__ */ new Map());
|
|
369
372
|
const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
|
|
370
373
|
if (!routeItem) return true;
|
|
371
|
-
const currentCacheTimestamp =
|
|
374
|
+
const currentCacheTimestamp = timestampMapRef.current[pathname];
|
|
372
375
|
if (!currentCacheTimestamp) return false;
|
|
373
376
|
if (!routeItem.staleTime) return true;
|
|
374
377
|
return Date.now() - currentCacheTimestamp < routeItem.staleTime;
|
|
375
378
|
}, []);
|
|
376
|
-
const revalidateCache = useCallback(async ({ routeItem,
|
|
377
|
-
if (!routeItem?.loader)
|
|
378
|
-
|
|
379
|
-
if (!routeItem?.beforeLoad) loaderState.current = {};
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
379
|
+
const revalidateCache = useCallback(async ({ routeItem, pathname }) => {
|
|
380
|
+
if (!routeItem?.loader) return;
|
|
381
|
+
if (loadingPromises.current.has(pathname)) return loadingPromises.current.get(pathname);
|
|
382
382
|
if (isCacheItemFresh({
|
|
383
383
|
routeItem,
|
|
384
384
|
pathname
|
|
385
385
|
})) {
|
|
386
|
-
|
|
386
|
+
loaderStateRef.current = loaderMapRef.current[pathname];
|
|
387
387
|
return;
|
|
388
388
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
params
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
389
|
+
const promise = (async () => {
|
|
390
|
+
if (!routeItem?.loader) return;
|
|
391
|
+
try {
|
|
392
|
+
const params = getParamsObject({
|
|
393
|
+
params: routeItem.params,
|
|
394
|
+
pathname
|
|
395
|
+
});
|
|
396
|
+
const result = await routeItem?.loader({
|
|
397
|
+
params,
|
|
398
|
+
context,
|
|
399
|
+
setContext
|
|
400
|
+
});
|
|
401
|
+
timestampMapRef.current = {
|
|
402
|
+
...timestampMapRef.current,
|
|
403
|
+
[pathname]: Date.now()
|
|
404
|
+
};
|
|
405
|
+
loaderMapRef.current[pathname] = {
|
|
406
|
+
data: result,
|
|
407
|
+
loaderError: null,
|
|
408
|
+
beforeLoadError: null
|
|
409
|
+
};
|
|
410
|
+
loaderStateRef.current = {
|
|
411
|
+
...loaderStateRef?.current,
|
|
412
|
+
data: result,
|
|
413
|
+
loaderError: null
|
|
414
|
+
};
|
|
415
|
+
} catch (error) {
|
|
416
|
+
loaderStateRef.current = {
|
|
417
|
+
...loaderStateRef?.current,
|
|
418
|
+
data: null,
|
|
419
|
+
loaderError: error
|
|
420
|
+
};
|
|
421
|
+
} finally {
|
|
422
|
+
loadingPromises.current.delete(pathname);
|
|
423
|
+
}
|
|
424
|
+
})();
|
|
425
|
+
loadingPromises.current.set(pathname, promise);
|
|
426
|
+
return promise;
|
|
421
427
|
}, [
|
|
422
428
|
context,
|
|
423
429
|
isCacheItemFresh,
|
|
@@ -433,30 +439,28 @@ var useLoader = ({ routeList, context, setContext }) => {
|
|
|
433
439
|
}, [revalidateCache, routeList]),
|
|
434
440
|
revalidateCache,
|
|
435
441
|
isCacheItemFresh,
|
|
436
|
-
|
|
437
|
-
setIsLoading
|
|
442
|
+
loaderStateRef
|
|
438
443
|
};
|
|
439
444
|
};
|
|
440
445
|
//#endregion
|
|
441
446
|
//#region components/RouterProvider.tsx
|
|
442
447
|
var RouterProvider = ({ children, routeList, context: initialContext = {} }) => {
|
|
443
448
|
const [context, setContext] = useState(initialContext);
|
|
444
|
-
const { prefetchLoader, revalidateCache, isCacheItemFresh,
|
|
449
|
+
const { prefetchLoader, revalidateCache, isCacheItemFresh, loaderStateRef } = useLoader({
|
|
445
450
|
routeList,
|
|
446
451
|
context,
|
|
447
452
|
setContext
|
|
448
453
|
});
|
|
449
|
-
const { blockerState, updateLocation, updateBlockedRoute, routeItemData,
|
|
454
|
+
const { blockerState, updateLocation, updateBlockedRoute, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState } = useHandleNavigation({
|
|
450
455
|
routeList,
|
|
451
456
|
context,
|
|
452
457
|
setContext,
|
|
453
458
|
revalidateCache,
|
|
454
459
|
isCacheItemFresh,
|
|
455
|
-
|
|
460
|
+
loaderStateRef
|
|
456
461
|
});
|
|
457
462
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
|
|
458
463
|
...useMemo(() => ({
|
|
459
|
-
setSearch,
|
|
460
464
|
updateLocation,
|
|
461
465
|
prefetchLoader,
|
|
462
466
|
updateBlockedRoute,
|
|
@@ -466,18 +470,19 @@ var RouterProvider = ({ children, routeList, context: initialContext = {} }) =>
|
|
|
466
470
|
routeItemData,
|
|
467
471
|
restoreScroll,
|
|
468
472
|
currentLoaderFallback,
|
|
469
|
-
isLoading
|
|
473
|
+
isLoading,
|
|
474
|
+
loaderState
|
|
470
475
|
}), [
|
|
471
476
|
blockerState,
|
|
472
477
|
context,
|
|
473
478
|
prefetchLoader,
|
|
474
479
|
routeItemData,
|
|
475
|
-
setSearch,
|
|
476
480
|
updateBlockedRoute,
|
|
477
481
|
updateLocation,
|
|
478
482
|
currentLoaderFallback,
|
|
479
483
|
restoreScroll,
|
|
480
|
-
isLoading
|
|
484
|
+
isLoading,
|
|
485
|
+
loaderState
|
|
481
486
|
]),
|
|
482
487
|
children
|
|
483
488
|
});
|
|
@@ -543,14 +548,6 @@ var usePreserveScroll = (preserveScroll) => {
|
|
|
543
548
|
]);
|
|
544
549
|
};
|
|
545
550
|
//#endregion
|
|
546
|
-
//#region hooks/useSetIsAnimated.ts
|
|
547
|
-
var useSetIsAnimated = (isAnimated = false, showFallbackIfAnimated) => {
|
|
548
|
-
useEffect(() => routerConfig.configure({
|
|
549
|
-
isAnimated,
|
|
550
|
-
showFallbackIfAnimated
|
|
551
|
-
}), [isAnimated]);
|
|
552
|
-
};
|
|
553
|
-
//#endregion
|
|
554
551
|
//#region components/Spinner.tsx
|
|
555
552
|
var Spinner = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cr-spinner" });
|
|
556
553
|
//#endregion
|
|
@@ -560,12 +557,22 @@ var renderElement = (Component) => {
|
|
|
560
557
|
return typeof Component === "function" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, {}) : Component;
|
|
561
558
|
};
|
|
562
559
|
//#endregion
|
|
560
|
+
//#region hooks/useSetRouterConfig.ts
|
|
561
|
+
var useSetRouterConfig = (routerProps) => {
|
|
562
|
+
useEffect(() => routerConfig.configure(routerProps), [routerProps]);
|
|
563
|
+
};
|
|
564
|
+
//#endregion
|
|
563
565
|
//#region components/Router.tsx
|
|
564
|
-
var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true, showFallbackIfAnimated = false }) => {
|
|
565
|
-
const { routeItemData: { routeItem, loaderState
|
|
566
|
+
var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true, showFallbackIfAnimated = false, prefetch = "hover", hoverPrefetchDelay = 150 }) => {
|
|
567
|
+
const { routeItemData: { routeItem }, loaderState, currentLoaderFallback, isLoading } = useNavigationState();
|
|
566
568
|
usePreserveScroll(preserveScroll);
|
|
567
569
|
useApplyCustomAnimation(animationDuration);
|
|
568
|
-
|
|
570
|
+
useSetRouterConfig({
|
|
571
|
+
isAnimated,
|
|
572
|
+
showFallbackIfAnimated,
|
|
573
|
+
prefetch,
|
|
574
|
+
hoverPrefetchDelay
|
|
575
|
+
});
|
|
569
576
|
const showErrorElement = !isLoading && Boolean(loaderState.loaderError || loaderState.beforeLoadError);
|
|
570
577
|
const showSpinner = spinner && isAnimated && isLoading;
|
|
571
578
|
const loadingContent = !showErrorElement && isLoading;
|
|
@@ -598,31 +605,63 @@ var useNavigate = () => {
|
|
|
598
605
|
};
|
|
599
606
|
//#endregion
|
|
600
607
|
//#region components/Link.tsx
|
|
601
|
-
var
|
|
602
|
-
|
|
608
|
+
var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }) => {
|
|
609
|
+
const { prefetch: configPrefetch, hoverPrefetchDelay: configPrefetchDelay } = routerConfig;
|
|
610
|
+
const prefetch = prefetchLink || configPrefetch;
|
|
611
|
+
const prefetchDelay = hoverPrefetchDelay ?? configPrefetchDelay;
|
|
603
612
|
const { prefetchLoader } = useRouterActions();
|
|
604
613
|
const navigate = useNavigate();
|
|
605
614
|
const timeout = useRef(0);
|
|
615
|
+
const ref = useRef(null);
|
|
616
|
+
const onMouseEnter = useCallback(() => {
|
|
617
|
+
if (prefetch !== "hover" || !prefetchDelay) return;
|
|
618
|
+
if (timeout.current) clearTimeout(timeout.current);
|
|
619
|
+
timeout.current = window.setTimeout(() => prefetchLoader(to), prefetchDelay);
|
|
620
|
+
}, [
|
|
621
|
+
prefetch,
|
|
622
|
+
prefetchDelay,
|
|
623
|
+
prefetchLoader,
|
|
624
|
+
to
|
|
625
|
+
]);
|
|
626
|
+
const onMouseLeave = useCallback(() => {
|
|
627
|
+
if (prefetch !== "hover" || !prefetchDelay) return;
|
|
628
|
+
if (timeout.current) {
|
|
629
|
+
clearTimeout(timeout.current);
|
|
630
|
+
timeout.current = 0;
|
|
631
|
+
}
|
|
632
|
+
}, [prefetch, prefetchDelay]);
|
|
633
|
+
useEffect(() => {
|
|
634
|
+
if (prefetch !== "render") return;
|
|
635
|
+
(async () => {
|
|
636
|
+
await prefetchLoader(to);
|
|
637
|
+
})();
|
|
638
|
+
}, [
|
|
639
|
+
prefetch,
|
|
640
|
+
prefetchLoader,
|
|
641
|
+
to
|
|
642
|
+
]);
|
|
643
|
+
useEffect(() => {
|
|
644
|
+
if (prefetch !== "viewport") return;
|
|
645
|
+
const element = ref.current;
|
|
646
|
+
const observer = new IntersectionObserver(async () => {
|
|
647
|
+
await prefetchLoader(to);
|
|
648
|
+
observer.disconnect();
|
|
649
|
+
});
|
|
650
|
+
if (element) observer.observe(element);
|
|
651
|
+
return () => {
|
|
652
|
+
if (element) observer.disconnect();
|
|
653
|
+
};
|
|
654
|
+
}, [
|
|
655
|
+
prefetch,
|
|
656
|
+
prefetchLoader,
|
|
657
|
+
to
|
|
658
|
+
]);
|
|
606
659
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
|
|
660
|
+
ref,
|
|
607
661
|
style: { cursor: "pointer" },
|
|
608
662
|
onClick: () => navigate(to),
|
|
609
|
-
onMouseEnter
|
|
610
|
-
|
|
611
|
-
if (timeout.current) clearTimeout(timeout.current);
|
|
612
|
-
timeout.current = window.setTimeout(() => prefetchLoader(to), prefetchDelay);
|
|
613
|
-
}, [
|
|
614
|
-
prefetch,
|
|
615
|
-
prefetchDelay,
|
|
616
|
-
prefetchLoader,
|
|
617
|
-
to
|
|
618
|
-
]),
|
|
619
|
-
onMouseLeave: useCallback(() => {
|
|
620
|
-
if (!prefetch || !prefetchDelay) return;
|
|
621
|
-
if (timeout.current) {
|
|
622
|
-
clearTimeout(timeout.current);
|
|
623
|
-
timeout.current = 0;
|
|
624
|
-
}
|
|
625
|
-
}, [prefetch, prefetchDelay]),
|
|
663
|
+
onMouseEnter,
|
|
664
|
+
onMouseLeave,
|
|
626
665
|
children
|
|
627
666
|
});
|
|
628
667
|
};
|
|
@@ -639,7 +678,7 @@ var useParams = () => {
|
|
|
639
678
|
//#endregion
|
|
640
679
|
//#region hooks/useLoaderState.ts
|
|
641
680
|
var useLoaderState = () => {
|
|
642
|
-
const {
|
|
681
|
+
const { loaderState } = useNavigationState();
|
|
643
682
|
return loaderState;
|
|
644
683
|
};
|
|
645
684
|
//#endregion
|
|
@@ -687,12 +726,40 @@ var useRouterContext = () => {
|
|
|
687
726
|
};
|
|
688
727
|
};
|
|
689
728
|
//#endregion
|
|
729
|
+
//#region hooks/useSearch.ts
|
|
730
|
+
var notify = () => {
|
|
731
|
+
window.dispatchEvent(new Event("locationChange"));
|
|
732
|
+
};
|
|
733
|
+
var patchHistory = () => {
|
|
734
|
+
const push = history.pushState;
|
|
735
|
+
const replace = history.replaceState;
|
|
736
|
+
history.pushState = function(...args) {
|
|
737
|
+
const result = push.apply(this, args);
|
|
738
|
+
notify();
|
|
739
|
+
return result;
|
|
740
|
+
};
|
|
741
|
+
history.replaceState = function(...args) {
|
|
742
|
+
const result = replace.apply(this, args);
|
|
743
|
+
notify();
|
|
744
|
+
return result;
|
|
745
|
+
};
|
|
746
|
+
window.addEventListener("popstate", notify);
|
|
747
|
+
};
|
|
748
|
+
var subscribe = (onChange) => {
|
|
749
|
+
window.addEventListener("locationChange", onChange);
|
|
750
|
+
return () => window.removeEventListener("locationChange", onChange);
|
|
751
|
+
};
|
|
752
|
+
var getSnapshot = () => window.location.search;
|
|
753
|
+
var useSearch = () => {
|
|
754
|
+
useEffect(patchHistory, []);
|
|
755
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
756
|
+
};
|
|
757
|
+
//#endregion
|
|
690
758
|
//#region hooks/useSearchParams.ts
|
|
691
759
|
var useSearchParams = () => {
|
|
692
|
-
const
|
|
693
|
-
const
|
|
694
|
-
const
|
|
695
|
-
const searchString = useMemo(() => location.search ? location.search.replace("?", "") : location.pathname.split("?")?.[1] ?? "", [location.pathname, location.search]);
|
|
760
|
+
const search = useSearch();
|
|
761
|
+
const searchRef = useLatest(search);
|
|
762
|
+
const searchString = search ? search.replace("?", "") : window.location.pathname.split("?")?.[1] ?? "";
|
|
696
763
|
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
|
|
697
764
|
const getSearchParams = useCallback((param) => {
|
|
698
765
|
const allValues = searchParams.getAll(param);
|
|
@@ -700,24 +767,22 @@ var useSearchParams = () => {
|
|
|
700
767
|
}, [searchParams]);
|
|
701
768
|
const navigateWithSearchParams = useCallback((params) => {
|
|
702
769
|
const newSearch = params.toString();
|
|
703
|
-
const { pathname } =
|
|
770
|
+
const { pathname } = window.location;
|
|
704
771
|
const search = newSearch ? `?${newSearch}` : "";
|
|
705
|
-
setSearch(search);
|
|
706
772
|
history.replaceState(null, "", pathname + search);
|
|
707
|
-
}, [
|
|
773
|
+
}, []);
|
|
708
774
|
return {
|
|
709
775
|
searchParams,
|
|
710
776
|
getSearchParams,
|
|
711
777
|
setSearchParams: useCallback((param, value) => {
|
|
712
|
-
const
|
|
713
|
-
const currentParams = new URLSearchParams(search);
|
|
778
|
+
const currentParams = new URLSearchParams(searchRef.current);
|
|
714
779
|
if (typeof param === "string" && value !== void 0) {
|
|
715
780
|
currentParams.delete(param);
|
|
716
781
|
(Array.isArray(value) ? value : [value]).forEach((v) => currentParams.append(param, v));
|
|
717
782
|
navigateWithSearchParams(currentParams);
|
|
718
783
|
} else if (typeof param === "function") navigateWithSearchParams(param(currentParams));
|
|
719
784
|
else throw new Error("useSearchParams first argument must be either function or string");
|
|
720
|
-
}, [
|
|
785
|
+
}, [navigateWithSearchParams, searchRef])
|
|
721
786
|
};
|
|
722
787
|
};
|
|
723
788
|
//#endregion
|
|
@@ -3,5 +3,5 @@ import { type ActionsContextValue, type DataContextValue, type NavigationContext
|
|
|
3
3
|
type ProviderProps = NavigationContextValue & ActionsContextValue & DataContextValue & {
|
|
4
4
|
children: ReactNode;
|
|
5
5
|
};
|
|
6
|
-
export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation,
|
|
6
|
+
export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation, prefetchLoader, blockerState, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
7
|
export {};
|
package/dist/types/global.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentType, Dispatch, ReactElement,
|
|
1
|
+
import type { ComponentType, Dispatch, ReactElement, SetStateAction } from 'react';
|
|
2
2
|
export type LazyComponent = () => Promise<{
|
|
3
3
|
default: ComponentType<unknown>;
|
|
4
4
|
}>;
|
|
@@ -48,7 +48,6 @@ export type UpdateBlockedRouteProps = {
|
|
|
48
48
|
export type RevalidateCacheArgs = {
|
|
49
49
|
pathname: string;
|
|
50
50
|
routeItem?: RouteItem;
|
|
51
|
-
loaderState?: RefObject<LoaderState>;
|
|
52
51
|
};
|
|
53
52
|
export type LoaderState = {
|
|
54
53
|
data: unknown;
|
|
@@ -62,5 +61,13 @@ export type AdapterType<T> = {
|
|
|
62
61
|
export type RouteItemData = {
|
|
63
62
|
location: Location;
|
|
64
63
|
routeItem: RouteItem | undefined;
|
|
65
|
-
|
|
64
|
+
};
|
|
65
|
+
export type RouterProps = {
|
|
66
|
+
isAnimated?: boolean;
|
|
67
|
+
animationDuration?: number;
|
|
68
|
+
spinner?: boolean;
|
|
69
|
+
preserveScroll?: boolean;
|
|
70
|
+
showFallbackIfAnimated?: boolean;
|
|
71
|
+
prefetch?: 'hover' | 'render' | 'viewport' | 'none';
|
|
72
|
+
hoverPrefetchDelay?: number;
|
|
66
73
|
};
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const useSetIsAnimated: (isAnimated: boolean | undefined, showFallbackIfAnimated: boolean) => void;
|