clear-react-router 1.3.2 → 1.3.4
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 +21 -6
- package/dist/components/RouterProvider.d.ts +2 -1
- package/dist/hooks/useHandleNavigation.d.ts +4 -1
- package/dist/hooks/useLoader.d.ts +3 -1
- package/dist/hooks/usePreserveScroll.d.ts +6 -0
- package/dist/index.js +61 -19
- package/dist/types/global.d.ts +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ A lightweight, type-safe routing library for React applications with nested rout
|
|
|
12
12
|
- 🎯 **Type-safe Redirects** - Redirect from beforeLoad hook
|
|
13
13
|
- 📦 **Prefetching** - Preload data on hover for instant navigation
|
|
14
14
|
- 🚀 **Lazy Loading** - Code-split your routes with dynamic imports for optimal performance
|
|
15
|
+
- 📍 **Scroll Restoration** — Automatically saves and restores scroll position when navigating back to a page (preserves user's scroll position)
|
|
15
16
|
- 🎨 **Flexible API** - Use components or hooks as you prefer
|
|
16
17
|
- 📱 **Browser History** - Full support for browser back/forward buttons
|
|
17
18
|
- 🧠 **Context-aware** - Pass and update context through routes
|
|
@@ -26,9 +27,9 @@ Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic pa
|
|
|
26
27
|
|----------|------|-------------|
|
|
27
28
|
| `path` | `string` | Route path, e.g., `/user/:userId` |
|
|
28
29
|
| `element` | `ReactElement \| () => ReactElement \| LazyComponent` | Component to render |
|
|
29
|
-
| `loader` | `({ params, context }) => Promise<unknown>` | Fetch data using route params and context |
|
|
30
|
-
| `beforeLoad` | `({ params, context, redirect }) => Promise<unknown> \| undefined` | Auth checks and redirects. `redirect` is provided by the router |
|
|
31
|
-
| `afterLoad` | `({ params, context }) => Promise<void>` | Analytics, side effects after data is loaded |
|
|
30
|
+
| `loader` | `({ params, context, setContext }) => Promise<unknown>` | Fetch data using route params and context. Can update context via `setContext` |
|
|
31
|
+
| `beforeLoad` | `({ params, context, redirect, setContext }) => Promise<unknown> \| undefined \| void` | Auth checks and redirects. Can update context via `setContext`. `redirect` is provided by the router |
|
|
32
|
+
| `afterLoad` | `({ params, context, setContext }) => Promise<void>` | Analytics, side effects after data is loaded. Can update context via `setContext` |
|
|
32
33
|
| `fallback` | `ReactElement \| () => ReactElement` | Loading fallback (for lazy loading) |
|
|
33
34
|
| `loaderFallback` | `ReactElement \| () => ReactElement` | Loading fallback (for loader) |
|
|
34
35
|
| `errorElement` | `ReactElement \| () => ReactElement` | Error fallback |
|
|
@@ -45,6 +46,7 @@ The root component that provides routing context to the application. Place stati
|
|
|
45
46
|
| `context` | `object` | `{}` | Initial context (user, theme, etc.) |
|
|
46
47
|
| `isAnimated` | `boolean` | `false` | Enable smooth page transitions |
|
|
47
48
|
| `animationDuration` | `number` | `optional` | Animation duration in milliseconds (browser default is used if not set) |
|
|
49
|
+
| `preserveScroll` | `boolean` | `true` | Save and restore scroll position when navigating between pages |
|
|
48
50
|
| `children` | `ReactNode` | required | App content (must include `<Router />`) |
|
|
49
51
|
|
|
50
52
|
```
|
|
@@ -95,7 +97,7 @@ Function provided to `beforeLoad` for programmatic redirection.
|
|
|
95
97
|
**Type:** `(arg: Location | string) => Promise<void>`
|
|
96
98
|
|
|
97
99
|
```
|
|
98
|
-
import type {
|
|
100
|
+
import type { createRouter } from 'clear-react-router';
|
|
99
101
|
|
|
100
102
|
const routes = createRouter([
|
|
101
103
|
{
|
|
@@ -107,8 +109,6 @@ const routes = createRouter([
|
|
|
107
109
|
},
|
|
108
110
|
]);
|
|
109
111
|
|
|
110
|
-
or
|
|
111
|
-
|
|
112
112
|
const routes = createRouter([
|
|
113
113
|
{
|
|
114
114
|
path: '/dashboard',
|
|
@@ -118,6 +118,21 @@ const routes = createRouter([
|
|
|
118
118
|
},
|
|
119
119
|
},
|
|
120
120
|
]);
|
|
121
|
+
|
|
122
|
+
const routes = createRouter([
|
|
123
|
+
{
|
|
124
|
+
path: '/user/:userId',
|
|
125
|
+
loader: async ({ params, context, setContext }) => {
|
|
126
|
+
const user = await fetchUser(params.userId);
|
|
127
|
+
setContext({ ...context, currentUser: user });
|
|
128
|
+
return { user };
|
|
129
|
+
},
|
|
130
|
+
beforeLoad: async ({ context, setContext, redirect }) => {
|
|
131
|
+
if (!context.token) return redirect('/login');
|
|
132
|
+
setContext({ ...context, lastVisit: Date.now() });
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
]);
|
|
121
136
|
|
|
122
137
|
```
|
|
123
138
|
|
|
@@ -6,6 +6,7 @@ type RouteProviderProps = {
|
|
|
6
6
|
context?: Record<string, unknown>;
|
|
7
7
|
isAnimated?: boolean;
|
|
8
8
|
animationDuration?: number;
|
|
9
|
+
preserveScroll?: boolean;
|
|
9
10
|
};
|
|
10
|
-
export declare const RouterProvider: ({ children, routeList, context: initialContext, isAnimated, animationDuration, }: RouteProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export declare const RouterProvider: ({ children, routeList, context: initialContext, isAnimated, animationDuration, preserveScroll, }: RouteProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
12
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Dispatch, type SetStateAction } from 'react';
|
|
1
2
|
import type { BlockerState, Location, RevalidateCacheArgs, RouteItem, UpdateBlockedRouteProps } from '../types/global';
|
|
2
3
|
type UseHandleNavigation = {
|
|
3
4
|
routeList: RouteItem[];
|
|
@@ -5,8 +6,10 @@ type UseHandleNavigation = {
|
|
|
5
6
|
context: Record<string, unknown>;
|
|
6
7
|
revalidateCache(arg: RevalidateCacheArgs): Promise<void>;
|
|
7
8
|
isAnimated: boolean;
|
|
9
|
+
setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
|
|
10
|
+
setScrollMap: Dispatch<SetStateAction<Record<string, number>>>;
|
|
8
11
|
};
|
|
9
|
-
export declare const useHandleNavigation: ({ setLocation, routeList, context, revalidateCache, isAnimated, }: UseHandleNavigation) => {
|
|
12
|
+
export declare const useHandleNavigation: ({ setLocation, routeList, context, revalidateCache, isAnimated, setContext, setScrollMap, }: UseHandleNavigation) => {
|
|
10
13
|
blockerState: BlockerState;
|
|
11
14
|
updateLocation: (nextLocation: Location) => Promise<void>;
|
|
12
15
|
updateBlockedRoute: ({ type, payload }: UpdateBlockedRouteProps) => void;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { type Dispatch, type SetStateAction } from 'react';
|
|
1
2
|
import type { RevalidateCacheArgs, RouteItem } from '../types/global';
|
|
2
3
|
type UseLoaderParams = {
|
|
3
4
|
routeList: RouteItem[];
|
|
4
5
|
context: Record<string, unknown>;
|
|
6
|
+
setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
|
|
5
7
|
};
|
|
6
|
-
export declare const useLoader: ({ routeList, context }: UseLoaderParams) => {
|
|
8
|
+
export declare const useLoader: ({ routeList, context, setContext }: UseLoaderParams) => {
|
|
7
9
|
loaderCache: unknown;
|
|
8
10
|
loaderError: boolean;
|
|
9
11
|
prefetchLoader: (pathname: string) => Promise<void>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type UsePreserveScrollParams = {
|
|
2
|
+
pathname: string;
|
|
3
|
+
preserveScroll: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare const usePreserveScroll: ({ pathname, preserveScroll }: UsePreserveScrollParams) => import("react").Dispatch<import("react").SetStateAction<Record<string, number>>>;
|
|
6
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -77,6 +77,15 @@ var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocat
|
|
|
77
77
|
});
|
|
78
78
|
};
|
|
79
79
|
//#endregion
|
|
80
|
+
//#region hooks/useLatest.ts
|
|
81
|
+
var useLatest = (value) => {
|
|
82
|
+
const ref = useRef(value);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
ref.current = value;
|
|
85
|
+
}, [value]);
|
|
86
|
+
return ref;
|
|
87
|
+
};
|
|
88
|
+
//#endregion
|
|
80
89
|
//#region utils/createLazyComponent.tsx
|
|
81
90
|
var createLazyComponent = (importFn, fallback) => {
|
|
82
91
|
const LazyComp = lazy(() => importFn().then((module) => ({ default: module.default || module })));
|
|
@@ -128,17 +137,8 @@ var comparePaths = (el, pathname) => {
|
|
|
128
137
|
return splitElementPath.every((item, index) => item === splitPathname[index + (index ? 1 : 0)]) && splitPathname.length === splitElementPath.length + paramsLength;
|
|
129
138
|
};
|
|
130
139
|
//#endregion
|
|
131
|
-
//#region hooks/useLatest.ts
|
|
132
|
-
var useLatest = (value) => {
|
|
133
|
-
const ref = useRef(value);
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
ref.current = value;
|
|
136
|
-
}, [value]);
|
|
137
|
-
return ref;
|
|
138
|
-
};
|
|
139
|
-
//#endregion
|
|
140
140
|
//#region hooks/useHandleNavigation.ts
|
|
141
|
-
var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, isAnimated }) => {
|
|
141
|
+
var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, isAnimated, setContext, setScrollMap }) => {
|
|
142
142
|
const [blockedRoute, setBlockedRoute] = useState({
|
|
143
143
|
from: "",
|
|
144
144
|
to: ""
|
|
@@ -146,6 +146,14 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
|
|
|
146
146
|
const [beforeLoadError, setBeforeLoadError] = useState(false);
|
|
147
147
|
const prevPathname = useRef("");
|
|
148
148
|
const navigationSeq = useRef(0);
|
|
149
|
+
const updateScrollMap = useCallback(() => setScrollMap((prevState) => {
|
|
150
|
+
const scrollPosition = document.scrollingElement?.scrollTop ?? 0;
|
|
151
|
+
if (!scrollPosition || prevState[prevPathname.current] === scrollPosition) return prevState;
|
|
152
|
+
return {
|
|
153
|
+
...prevState,
|
|
154
|
+
[prevPathname.current]: scrollPosition
|
|
155
|
+
};
|
|
156
|
+
}), [setScrollMap]);
|
|
149
157
|
const navigation = useCallback((nextLocation) => {
|
|
150
158
|
setLocation(nextLocation);
|
|
151
159
|
prevPathname.current = nextLocation.pathname;
|
|
@@ -164,6 +172,7 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
|
|
|
164
172
|
const navigationHandler = useCallback(async (nextLocation, isFirstCall) => {
|
|
165
173
|
navigationSeq.current = navigationSeq.current + 1;
|
|
166
174
|
const seq = navigationSeq.current;
|
|
175
|
+
updateScrollMap();
|
|
167
176
|
const nextItem = routeList.find((el) => comparePaths(el, nextLocation.pathname));
|
|
168
177
|
const params = getParamsObject({
|
|
169
178
|
routeItem: nextItem,
|
|
@@ -174,7 +183,8 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
|
|
|
174
183
|
await nextItem.beforeLoad({
|
|
175
184
|
context,
|
|
176
185
|
redirect,
|
|
177
|
-
params
|
|
186
|
+
params,
|
|
187
|
+
setContext
|
|
178
188
|
});
|
|
179
189
|
} catch {
|
|
180
190
|
setBeforeLoadError(true);
|
|
@@ -199,14 +209,17 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
|
|
|
199
209
|
setBeforeLoadError(false);
|
|
200
210
|
if (nextItem?.afterLoad) await nextItem.afterLoad({
|
|
201
211
|
context,
|
|
202
|
-
params
|
|
212
|
+
params,
|
|
213
|
+
setContext
|
|
203
214
|
});
|
|
204
215
|
}, [
|
|
205
216
|
context,
|
|
206
217
|
revalidateCache,
|
|
207
218
|
routeList,
|
|
208
219
|
transitionedNavigation,
|
|
209
|
-
isAnimated
|
|
220
|
+
isAnimated,
|
|
221
|
+
setContext,
|
|
222
|
+
updateScrollMap
|
|
210
223
|
]);
|
|
211
224
|
const setNextLocationRef = useLatest(navigationHandler);
|
|
212
225
|
const updateBlockedRoute = useCallback(({ type, payload = "" }) => setBlockedRoute((prevState) => {
|
|
@@ -265,7 +278,7 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
|
|
|
265
278
|
};
|
|
266
279
|
//#endregion
|
|
267
280
|
//#region hooks/useLoader.ts
|
|
268
|
-
var useLoader = ({ routeList, context }) => {
|
|
281
|
+
var useLoader = ({ routeList, context, setContext }) => {
|
|
269
282
|
const [loaderCache, setLoaderCache] = useState();
|
|
270
283
|
const [loaderError, setLoaderError] = useState(false);
|
|
271
284
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -301,7 +314,8 @@ var useLoader = ({ routeList, context }) => {
|
|
|
301
314
|
});
|
|
302
315
|
const result = await routeItem?.loader({
|
|
303
316
|
params,
|
|
304
|
-
context
|
|
317
|
+
context,
|
|
318
|
+
setContext
|
|
305
319
|
});
|
|
306
320
|
cacheTimestampsRef.current = {
|
|
307
321
|
...cacheTimestampsRef.current,
|
|
@@ -317,7 +331,11 @@ var useLoader = ({ routeList, context }) => {
|
|
|
317
331
|
} finally {
|
|
318
332
|
if (isCurrentRoute) setIsLoading(false);
|
|
319
333
|
}
|
|
320
|
-
}, [
|
|
334
|
+
}, [
|
|
335
|
+
context,
|
|
336
|
+
isCacheItemFresh,
|
|
337
|
+
setContext
|
|
338
|
+
]);
|
|
321
339
|
return {
|
|
322
340
|
loaderCache,
|
|
323
341
|
loaderError,
|
|
@@ -333,6 +351,23 @@ var useLoader = ({ routeList, context }) => {
|
|
|
333
351
|
};
|
|
334
352
|
};
|
|
335
353
|
//#endregion
|
|
354
|
+
//#region hooks/usePreserveScroll.ts
|
|
355
|
+
var usePreserveScroll = ({ pathname, preserveScroll }) => {
|
|
356
|
+
const [scrollMap, setScrollMap] = useState({});
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
if (!preserveScroll || !pathname || !scrollMap[pathname]) return;
|
|
359
|
+
document.scrollingElement?.scrollTo({
|
|
360
|
+
top: scrollMap[pathname],
|
|
361
|
+
behavior: "smooth"
|
|
362
|
+
});
|
|
363
|
+
}, [
|
|
364
|
+
pathname,
|
|
365
|
+
scrollMap,
|
|
366
|
+
preserveScroll
|
|
367
|
+
]);
|
|
368
|
+
return setScrollMap;
|
|
369
|
+
};
|
|
370
|
+
//#endregion
|
|
336
371
|
//#region hooks/useApplyCustomAnimation.ts
|
|
337
372
|
var useApplyCustomAnimation = (animationDuration) => {
|
|
338
373
|
useEffect(() => {
|
|
@@ -371,20 +406,27 @@ var useApplyCustomAnimation = (animationDuration) => {
|
|
|
371
406
|
};
|
|
372
407
|
//#endregion
|
|
373
408
|
//#region components/RouterProvider.tsx
|
|
374
|
-
var RouterProvider = ({ children, routeList, context: initialContext = {}, isAnimated = false, animationDuration }) => {
|
|
409
|
+
var RouterProvider = ({ children, routeList, context: initialContext = {}, isAnimated = false, animationDuration, preserveScroll = true }) => {
|
|
375
410
|
const [location, setLocation] = useState({});
|
|
376
411
|
const [context, setContext] = useState(initialContext);
|
|
377
412
|
useApplyCustomAnimation(animationDuration);
|
|
413
|
+
const setScrollMap = usePreserveScroll({
|
|
414
|
+
pathname: location.pathname,
|
|
415
|
+
preserveScroll
|
|
416
|
+
});
|
|
378
417
|
const { loaderError, loaderCache, prefetchLoader, revalidateCache, isLoading } = useLoader({
|
|
379
418
|
routeList,
|
|
380
|
-
context
|
|
419
|
+
context,
|
|
420
|
+
setContext
|
|
381
421
|
});
|
|
382
422
|
const { blockerState, updateLocation, updateBlockedRoute, beforeLoadError } = useHandleNavigation({
|
|
383
423
|
setLocation,
|
|
384
424
|
routeList,
|
|
385
425
|
context,
|
|
426
|
+
setContext,
|
|
386
427
|
revalidateCache,
|
|
387
|
-
isAnimated
|
|
428
|
+
isAnimated,
|
|
429
|
+
setScrollMap
|
|
388
430
|
});
|
|
389
431
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
|
|
390
432
|
...useMemo(() => ({
|
package/dist/types/global.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentType, ReactElement } from 'react';
|
|
1
|
+
import type { ComponentType, Dispatch, ReactElement, SetStateAction } from 'react';
|
|
2
2
|
export type LazyComponent = () => Promise<{
|
|
3
3
|
default: ComponentType<unknown>;
|
|
4
4
|
}>;
|
|
@@ -8,6 +8,7 @@ export type ClientRouteItem = {
|
|
|
8
8
|
loader?(arg: {
|
|
9
9
|
params: Record<string, string>;
|
|
10
10
|
context: Record<string, unknown>;
|
|
11
|
+
setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
|
|
11
12
|
}): Promise<unknown>;
|
|
12
13
|
loaderFallback?: (() => ReactElement) | ReactElement;
|
|
13
14
|
errorElement?: (() => ReactElement) | ReactElement;
|
|
@@ -18,10 +19,12 @@ export type ClientRouteItem = {
|
|
|
18
19
|
context: Record<string, unknown>;
|
|
19
20
|
redirect: (arg: Location | string) => Promise<void>;
|
|
20
21
|
params: Record<string, string>;
|
|
21
|
-
|
|
22
|
+
setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
|
|
23
|
+
}) => Promise<unknown> | undefined | void;
|
|
22
24
|
afterLoad?: (arg: {
|
|
23
25
|
context: Record<string, unknown>;
|
|
24
26
|
params: Record<string, string>;
|
|
27
|
+
setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
|
|
25
28
|
}) => Promise<void>;
|
|
26
29
|
};
|
|
27
30
|
export type RouteItem = ClientRouteItem & {
|