clear-react-router 1.2.1 → 1.2.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 +43 -5
- package/dist/hooks/useHandleNavigation.d.ts +3 -2
- package/dist/hooks/useLoader.d.ts +8 -3
- package/dist/index.js +102 -31
- package/dist/types/global.d.ts +14 -2
- package/dist/utils/utils.d.ts +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,9 +25,9 @@ Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic pa
|
|
|
25
25
|
|----------|------|-------------|
|
|
26
26
|
| `path` | `string` | Route path, e.g., `/user/:userId` |
|
|
27
27
|
| `element` | `ReactElement \| () => ReactElement \| LazyComponent` | Component to render |
|
|
28
|
-
| `loader` | `() => Promise<unknown>` | Fetch data |
|
|
29
|
-
| `beforeLoad` | `({ context, redirect }) => Promise<
|
|
30
|
-
| `afterLoad` | `(context) => Promise<void>` | Analytics, side effects |
|
|
28
|
+
| `loader` | `({ params, context }) => Promise<unknown>` | Fetch data using route params and context |
|
|
29
|
+
| `beforeLoad` | `({ params, context, redirect }) => Promise<unknown> \| undefined` | Auth checks and redirects. `redirect` is provided by the router |
|
|
30
|
+
| `afterLoad` | `({ params, context }) => Promise<void>` | Analytics, side effects after data is loaded |
|
|
31
31
|
| `fallback` | `ReactElement \| () => ReactElement` | Loading fallback (for lazy loading) |
|
|
32
32
|
| `loaderFallback` | `ReactElement \| () => ReactElement` | Loading fallback (for loader) |
|
|
33
33
|
| `errorElement` | `ReactElement \| () => ReactElement` | Error fallback |
|
|
@@ -86,6 +86,38 @@ const routes = createRouter([
|
|
|
86
86
|
]);
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
### Usage with Parameters
|
|
90
|
+
|
|
91
|
+
The `loader`, `beforeLoad`, and `afterLoad` hooks receive `params` (extracted from the URL) and `context` as arguments. This allows you to handle route-specific logic directly in the route configuration, keeping your components focused on rendering.
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
const routes = createRouter([
|
|
95
|
+
{
|
|
96
|
+
path: '/user/:userId',
|
|
97
|
+
element: <UserProfile />,
|
|
98
|
+
loader: async ({ params, context }) => {
|
|
99
|
+
// params.userId is available from the URL
|
|
100
|
+
const user = await fetchUser(params.userId);
|
|
101
|
+
return { user };
|
|
102
|
+
},
|
|
103
|
+
beforeLoad: async ({ params, context, redirect }) => {
|
|
104
|
+
// Authentication check
|
|
105
|
+
if (!context.isAuthorized) {
|
|
106
|
+
return redirect({ pathname: '/login' });
|
|
107
|
+
}
|
|
108
|
+
// Validate parameter
|
|
109
|
+
if (!params.userId || !isValidUserId(params.userId)) {
|
|
110
|
+
return redirect({ pathname: '/users' });
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
afterLoad: ({ params, context }) => {
|
|
114
|
+
// Analytics or side effects
|
|
115
|
+
console.log(`User ${params.userId} loaded`);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
]);
|
|
119
|
+
```
|
|
120
|
+
|
|
89
121
|
## Hooks
|
|
90
122
|
|
|
91
123
|
### `useNavigate()`
|
|
@@ -111,8 +143,10 @@ console.log(state); // { userId: 123 }
|
|
|
111
143
|
|
|
112
144
|
Returns route parameters object.
|
|
113
145
|
|
|
146
|
+
```
|
|
114
147
|
const params = useParams<{ userId: string }>();
|
|
115
148
|
// URL: /user/123 → params.userId === '123'
|
|
149
|
+
```
|
|
116
150
|
|
|
117
151
|
### `useLocation()`
|
|
118
152
|
|
|
@@ -124,6 +158,9 @@ const { pathname, search, state } = useLocation();
|
|
|
124
158
|
### `useLoaderState()`
|
|
125
159
|
|
|
126
160
|
Returns loaderState from current route's loader.
|
|
161
|
+
```
|
|
162
|
+
const state = useLoaderState();
|
|
163
|
+
```
|
|
127
164
|
|
|
128
165
|
### `useBlocker(callback)`
|
|
129
166
|
|
|
@@ -173,6 +210,7 @@ const onSave = useCallback(() => {
|
|
|
173
210
|
// Auto-save when user tries to close/reload the page
|
|
174
211
|
useBeforeUnload(text ? onSave : undefined);
|
|
175
212
|
```
|
|
213
|
+
> **Note:** Pass `undefined` to disable the handler (e.g., if there is no changes).
|
|
176
214
|
|
|
177
215
|
### `useRouterContext()`
|
|
178
216
|
|
|
@@ -244,5 +282,5 @@ For older browsers, the router gracefully falls back to regular navigation witho
|
|
|
244
282
|
- React 16.6+ (for React.lazy and Suspense)
|
|
245
283
|
- Use `default` export for your lazy-loaded components
|
|
246
284
|
|
|
247
|
-
## License
|
|
248
|
-
MIT
|
|
285
|
+
## License
|
|
286
|
+
MIT
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import type { BlockerState, Location, RouteItem, UpdateBlockedRouteProps } from '../types/global
|
|
1
|
+
import type { BlockerState, Location, RevalidateCacheArgs, RouteItem, UpdateBlockedRouteProps } from '../types/global';
|
|
2
2
|
type UseHandleNavigation = {
|
|
3
3
|
routeList: RouteItem[];
|
|
4
4
|
setLocation: (arg: Location) => void;
|
|
5
5
|
context: Record<string, unknown>;
|
|
6
|
-
revalidateCache(
|
|
6
|
+
revalidateCache(arg: RevalidateCacheArgs): Promise<void>;
|
|
7
7
|
isAnimated: boolean;
|
|
8
8
|
};
|
|
9
9
|
export declare const useHandleNavigation: ({ setLocation, routeList, context, revalidateCache, isAnimated, }: UseHandleNavigation) => {
|
|
10
10
|
blockerState: BlockerState;
|
|
11
11
|
updateLocation: (nextLocation: Location) => Promise<void>;
|
|
12
12
|
updateBlockedRoute: ({ type, payload }: UpdateBlockedRouteProps) => void;
|
|
13
|
+
beforeLoadError: boolean;
|
|
13
14
|
};
|
|
14
15
|
export {};
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type { RouteItem } from '../types/global
|
|
2
|
-
|
|
1
|
+
import type { RevalidateCacheArgs, RouteItem } from '../types/global';
|
|
2
|
+
type UseLoaderParams = {
|
|
3
|
+
routeList: RouteItem[];
|
|
4
|
+
context: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
export declare const useLoader: ({ routeList, context }: UseLoaderParams) => {
|
|
3
7
|
loaderCache: unknown;
|
|
4
8
|
loaderError: boolean;
|
|
5
9
|
prefetchLoader: (pathname: string) => Promise<void>;
|
|
6
|
-
revalidateCache: (routeItem
|
|
10
|
+
revalidateCache: ({ routeItem, isCurrentRoute, pathname }: RevalidateCacheArgs) => Promise<void>;
|
|
7
11
|
isLoading: boolean;
|
|
8
12
|
};
|
|
13
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -98,10 +98,10 @@ var parseClientRouteItem = (el, parentParams = [], parentPath = "") => {
|
|
|
98
98
|
}, ...el.children?.flatMap((child) => parseClientRouteItem(child, currentParams, path)) || []];
|
|
99
99
|
};
|
|
100
100
|
var createRouter = (clientList) => clientList.flatMap((el) => parseClientRouteItem(el, []));
|
|
101
|
-
var getParamsObject = (
|
|
102
|
-
|
|
101
|
+
var getParamsObject = ({ routeItem, pathname }) => {
|
|
102
|
+
if (!routeItem?.params) return {};
|
|
103
103
|
const split = pathname.split("/");
|
|
104
|
-
return (params || []).map((el) => ({
|
|
104
|
+
return (routeItem.params || []).map((el) => ({
|
|
105
105
|
index: split.findIndex((item) => item === el.key),
|
|
106
106
|
value: el.value
|
|
107
107
|
})).reduce((acc, cur) => ({
|
|
@@ -135,6 +135,7 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
|
|
|
135
135
|
from: "",
|
|
136
136
|
to: ""
|
|
137
137
|
});
|
|
138
|
+
const [beforeLoadError, setBeforeLoadError] = useState(false);
|
|
138
139
|
const prevPathname = useRef("");
|
|
139
140
|
const navigationSeq = useRef(0);
|
|
140
141
|
const navigation = useCallback((nextLocation) => {
|
|
@@ -151,31 +152,53 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
|
|
|
151
152
|
}
|
|
152
153
|
else navigation(nextLocation);
|
|
153
154
|
}, [navigation]);
|
|
154
|
-
const
|
|
155
|
+
const navigationHandler = useCallback(async (nextLocation, isFirstCall) => {
|
|
155
156
|
navigationSeq.current = navigationSeq.current + 1;
|
|
156
157
|
const seq = navigationSeq.current;
|
|
157
158
|
const nextItem = routeList.find((el) => comparePaths(el, nextLocation.pathname));
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
const params = getParamsObject({
|
|
160
|
+
routeItem: nextItem,
|
|
161
|
+
pathname: nextLocation.pathname
|
|
161
162
|
});
|
|
163
|
+
if (nextItem?.beforeLoad) try {
|
|
164
|
+
await nextItem.beforeLoad({
|
|
165
|
+
context,
|
|
166
|
+
redirect: navigationHandler,
|
|
167
|
+
params
|
|
168
|
+
});
|
|
169
|
+
} catch {
|
|
170
|
+
setBeforeLoadError(true);
|
|
171
|
+
transitionedNavigation({
|
|
172
|
+
nextLocation,
|
|
173
|
+
isAnimated: false
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
162
177
|
if (seq !== navigationSeq.current) return;
|
|
163
|
-
await revalidateCache(
|
|
178
|
+
await revalidateCache({
|
|
179
|
+
routeItem: nextItem,
|
|
180
|
+
isCurrentRoute: true,
|
|
181
|
+
pathname: nextLocation.pathname
|
|
182
|
+
});
|
|
164
183
|
if (seq !== navigationSeq.current) return;
|
|
165
184
|
transitionedNavigation({
|
|
166
185
|
nextLocation,
|
|
167
|
-
|
|
168
|
-
|
|
186
|
+
isFirstCall,
|
|
187
|
+
isAnimated
|
|
188
|
+
});
|
|
189
|
+
setBeforeLoadError(false);
|
|
190
|
+
if (nextItem?.afterLoad) await nextItem.afterLoad({
|
|
191
|
+
context,
|
|
192
|
+
params
|
|
169
193
|
});
|
|
170
|
-
if (nextItem?.afterLoad) await nextItem.afterLoad(context);
|
|
171
194
|
}, [
|
|
172
195
|
context,
|
|
173
|
-
isAnimated,
|
|
174
196
|
revalidateCache,
|
|
175
197
|
routeList,
|
|
176
|
-
transitionedNavigation
|
|
198
|
+
transitionedNavigation,
|
|
199
|
+
isAnimated
|
|
177
200
|
]);
|
|
178
|
-
const setNextLocationRef = useLatest(
|
|
201
|
+
const setNextLocationRef = useLatest(navigationHandler);
|
|
179
202
|
const updateBlockedRoute = useCallback(({ type, payload = "" }) => setBlockedRoute((prevState) => {
|
|
180
203
|
if (prevState.from === payload && type === "charge") return prevState;
|
|
181
204
|
if (payload && prevState.from !== payload && type === "charge") return {
|
|
@@ -226,26 +249,33 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
|
|
|
226
249
|
return "unblocked";
|
|
227
250
|
}, [blockedRoute]),
|
|
228
251
|
updateLocation,
|
|
229
|
-
updateBlockedRoute
|
|
252
|
+
updateBlockedRoute,
|
|
253
|
+
beforeLoadError
|
|
230
254
|
};
|
|
231
255
|
};
|
|
232
256
|
//#endregion
|
|
233
257
|
//#region hooks/useLoader.ts
|
|
234
|
-
var useLoader = (routeList) => {
|
|
258
|
+
var useLoader = ({ routeList, context }) => {
|
|
235
259
|
const [loaderCache, setLoaderCache] = useState();
|
|
236
260
|
const [loaderError, setLoaderError] = useState(false);
|
|
237
261
|
const [isLoading, setIsLoading] = useState(false);
|
|
238
262
|
const cacheTimestampsRef = useRef({});
|
|
239
263
|
const loaderCacheRef = useRef({});
|
|
240
|
-
const isCacheItemFresh = useCallback((routeItem) => {
|
|
264
|
+
const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
|
|
241
265
|
if (!routeItem) return true;
|
|
242
|
-
const currentCacheTimestamp = cacheTimestampsRef.current[
|
|
266
|
+
const currentCacheTimestamp = cacheTimestampsRef.current[pathname];
|
|
243
267
|
return Boolean(currentCacheTimestamp && Date.now() - currentCacheTimestamp < (routeItem.staleTime || 0));
|
|
244
268
|
}, []);
|
|
245
|
-
const revalidateCache = useCallback(async (routeItem, isCurrentRoute) => {
|
|
269
|
+
const revalidateCache = useCallback(async ({ routeItem, isCurrentRoute, pathname }) => {
|
|
246
270
|
if (!routeItem?.loader) return;
|
|
247
|
-
if (isCacheItemFresh(
|
|
248
|
-
|
|
271
|
+
if (isCacheItemFresh({
|
|
272
|
+
routeItem,
|
|
273
|
+
pathname
|
|
274
|
+
}) && isCurrentRoute) setLoaderCache(loaderCacheRef.current[pathname]);
|
|
275
|
+
if (isCacheItemFresh({
|
|
276
|
+
routeItem,
|
|
277
|
+
pathname
|
|
278
|
+
})) return;
|
|
249
279
|
if (isCurrentRoute) setIsLoading(true);
|
|
250
280
|
loaderCacheRef.current = Object.keys(loaderCacheRef.current).filter((el) => el !== routeItem.path).reduce((acc, cur) => ({
|
|
251
281
|
...acc,
|
|
@@ -253,14 +283,21 @@ var useLoader = (routeList) => {
|
|
|
253
283
|
}), {});
|
|
254
284
|
try {
|
|
255
285
|
if (isCurrentRoute) setLoaderError(false);
|
|
256
|
-
const
|
|
286
|
+
const params = getParamsObject({
|
|
287
|
+
routeItem,
|
|
288
|
+
pathname
|
|
289
|
+
});
|
|
290
|
+
const result = await routeItem?.loader({
|
|
291
|
+
params,
|
|
292
|
+
context
|
|
293
|
+
});
|
|
257
294
|
cacheTimestampsRef.current = {
|
|
258
295
|
...cacheTimestampsRef.current,
|
|
259
|
-
[
|
|
296
|
+
[pathname]: Date.now()
|
|
260
297
|
};
|
|
261
298
|
loaderCacheRef.current = {
|
|
262
299
|
...loaderCacheRef.current,
|
|
263
|
-
[
|
|
300
|
+
[pathname]: result
|
|
264
301
|
};
|
|
265
302
|
if (isCurrentRoute) setLoaderCache(result);
|
|
266
303
|
} catch {
|
|
@@ -268,13 +305,16 @@ var useLoader = (routeList) => {
|
|
|
268
305
|
} finally {
|
|
269
306
|
if (isCurrentRoute) setIsLoading(false);
|
|
270
307
|
}
|
|
271
|
-
}, [isCacheItemFresh]);
|
|
308
|
+
}, [context, isCacheItemFresh]);
|
|
272
309
|
return {
|
|
273
310
|
loaderCache,
|
|
274
311
|
loaderError,
|
|
275
312
|
prefetchLoader: useCallback(async (pathname) => {
|
|
276
313
|
const item = routeList.find((el) => comparePaths(el, pathname));
|
|
277
|
-
if (item) await revalidateCache(
|
|
314
|
+
if (item) await revalidateCache({
|
|
315
|
+
routeItem: item,
|
|
316
|
+
pathname
|
|
317
|
+
});
|
|
278
318
|
}, [revalidateCache, routeList]),
|
|
279
319
|
revalidateCache,
|
|
280
320
|
isLoading
|
|
@@ -283,6 +323,31 @@ var useLoader = (routeList) => {
|
|
|
283
323
|
//#endregion
|
|
284
324
|
//#region hooks/useApplyCustomAnimation.ts
|
|
285
325
|
var useApplyCustomAnimation = (animationOptions) => {
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
const style = document.createElement("style");
|
|
328
|
+
style.id = "spinner-style";
|
|
329
|
+
style.textContent = `
|
|
330
|
+
@keyframes cr-spin {
|
|
331
|
+
0% { transform: rotate(0deg); }
|
|
332
|
+
100% { transform: rotate(360deg); }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.cr-spinner {
|
|
336
|
+
position: fixed;
|
|
337
|
+
top: 5px;
|
|
338
|
+
left: 5px;
|
|
339
|
+
z-index: 9999;
|
|
340
|
+
width: 1rem;
|
|
341
|
+
height: 1rem;
|
|
342
|
+
border: 2px solid gray;
|
|
343
|
+
border-bottom-color: transparent;
|
|
344
|
+
border-radius: 50%;
|
|
345
|
+
animation: cr-spin 1s linear infinite;
|
|
346
|
+
}
|
|
347
|
+
`;
|
|
348
|
+
document.head.appendChild(style);
|
|
349
|
+
return () => style.remove();
|
|
350
|
+
}, []);
|
|
286
351
|
useEffect(() => {
|
|
287
352
|
if (!animationOptions?.duration) return;
|
|
288
353
|
const style = document.createElement("style");
|
|
@@ -348,15 +413,21 @@ var Router = ({ routeList, context: initialContext = {}, isAnimated = false, ani
|
|
|
348
413
|
const [context, setContext] = useState(initialContext);
|
|
349
414
|
useApplyCustomAnimation(animationOptions);
|
|
350
415
|
const routeItem = useMemo(() => routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, location.pathname)), [location.pathname, routeList]);
|
|
351
|
-
const
|
|
352
|
-
|
|
416
|
+
const params = useMemo(() => getParamsObject({
|
|
417
|
+
routeItem,
|
|
418
|
+
pathname: window.location.pathname
|
|
419
|
+
}), [routeItem]);
|
|
420
|
+
const { loaderError, loaderCache, prefetchLoader, revalidateCache, isLoading } = useLoader({
|
|
421
|
+
routeList,
|
|
422
|
+
context
|
|
423
|
+
});
|
|
424
|
+
const { blockerState, updateLocation, updateBlockedRoute, beforeLoadError } = useHandleNavigation({
|
|
353
425
|
setLocation,
|
|
354
426
|
routeList,
|
|
355
427
|
context,
|
|
356
428
|
revalidateCache,
|
|
357
429
|
isAnimated
|
|
358
430
|
});
|
|
359
|
-
const params = useMemo(() => routeItem?.params ? getParamsObject(routeItem.params) : {}, [routeItem]);
|
|
360
431
|
const providerProps = useMemo(() => ({
|
|
361
432
|
location,
|
|
362
433
|
updateLocation,
|
|
@@ -381,9 +452,9 @@ var Router = ({ routeList, context: initialContext = {}, isAnimated = false, ani
|
|
|
381
452
|
...providerProps,
|
|
382
453
|
children: renderElement(routeItem?.loaderFallback)
|
|
383
454
|
});
|
|
384
|
-
if (loaderError) return /* @__PURE__ */ (0, import_jsx_runtime.
|
|
455
|
+
if (loaderError || beforeLoadError) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RouterProvider, {
|
|
385
456
|
...providerProps,
|
|
386
|
-
children: renderElement(routeItem?.errorElement)
|
|
457
|
+
children: [renderElement(routeItem?.errorElement), isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
|
|
387
458
|
});
|
|
388
459
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RouterProvider, {
|
|
389
460
|
...providerProps,
|
package/dist/types/global.d.ts
CHANGED
|
@@ -5,7 +5,10 @@ export type LazyComponent = () => Promise<{
|
|
|
5
5
|
export type ClientRouteItem = {
|
|
6
6
|
path: string;
|
|
7
7
|
element: (() => ReactElement) | ReactElement | LazyComponent;
|
|
8
|
-
loader?(
|
|
8
|
+
loader?(arg: {
|
|
9
|
+
params: Record<string, string>;
|
|
10
|
+
context: Record<string, unknown>;
|
|
11
|
+
}): Promise<unknown>;
|
|
9
12
|
loaderFallback?: (() => ReactElement) | ReactElement;
|
|
10
13
|
errorElement?: (() => ReactElement) | ReactElement;
|
|
11
14
|
fallback?: (() => ReactElement) | ReactElement;
|
|
@@ -14,8 +17,12 @@ export type ClientRouteItem = {
|
|
|
14
17
|
beforeLoad?: (arg: {
|
|
15
18
|
context: Record<string, unknown>;
|
|
16
19
|
redirect: (arg: Location) => Promise<void>;
|
|
20
|
+
params: Record<string, string>;
|
|
17
21
|
}) => Promise<unknown> | undefined;
|
|
18
|
-
afterLoad?: (
|
|
22
|
+
afterLoad?: (arg: {
|
|
23
|
+
context: Record<string, unknown>;
|
|
24
|
+
params: Record<string, string>;
|
|
25
|
+
}) => Promise<void>;
|
|
19
26
|
};
|
|
20
27
|
export type RouteItem = ClientRouteItem & {
|
|
21
28
|
element: (() => ReactElement) | ReactElement;
|
|
@@ -39,3 +46,8 @@ export type AnimationOptions = {
|
|
|
39
46
|
duration?: number;
|
|
40
47
|
name?: 'fade' | 'slide-left' | 'slide-right';
|
|
41
48
|
};
|
|
49
|
+
export type RevalidateCacheArgs = {
|
|
50
|
+
pathname: string;
|
|
51
|
+
routeItem?: RouteItem;
|
|
52
|
+
isCurrentRoute?: boolean;
|
|
53
|
+
};
|
package/dist/utils/utils.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import type { ClientRouteItem, Location, RouteItem } from '../types/global
|
|
1
|
+
import type { ClientRouteItem, Location, RouteItem } from '../types/global';
|
|
2
2
|
export declare const createRouter: (clientList: ClientRouteItem[]) => RouteItem[];
|
|
3
|
-
export declare const getParamsObject: (
|
|
3
|
+
export declare const getParamsObject: ({ routeItem, pathname }: {
|
|
4
|
+
routeItem?: RouteItem;
|
|
5
|
+
pathname: string;
|
|
6
|
+
}) => {};
|
|
4
7
|
export declare const parseWindowLocation: (location: typeof window.location) => Location;
|
|
5
8
|
export declare const comparePaths: (el: RouteItem, pathname: string) => boolean;
|