clear-react-router 1.8.0 → 1.8.1

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 CHANGED
@@ -38,23 +38,6 @@ It provides first-class support for:
38
38
 
39
39
  ## API
40
40
 
41
- ### `createRouter(routes)`
42
-
43
- Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic params, builds nested paths.
44
-
45
- | Property | Type | Description |
46
- |----------|------|-------------|
47
- | `path` | `string` | Route path, e.g., `/user/:userId` |
48
- | `element` | `ReactElement \| () => ReactElement \| LazyComponent` | Component to render |
49
- | `beforeLoad` | `({ params, context, redirect, setContext }) => Promise<unknown> \| undefined \| void` | Auth checks and redirects. Can update context via `setContext`. `redirect` is provided by the router |
50
- | `loader` | `({ params, context, setContext }) => Promise<unknown>` | Fetch data using route params and context. Can update context via `setContext` |
51
- | `afterLoad` | `({ params, context, setContext }) => Promise<void>` | Analytics, side effects after data is loaded. Can update context via `setContext` |
52
- | `fallback` | `ReactElement \| () => ReactElement` | Loading fallback (for lazy loading) |
53
- | `loaderFallback` | `ReactElement \| () => ReactElement` | Loading fallback for the route's `loader`. Overrides the global `defaultLoaderFallback` set in `Router` |
54
- | `errorElement` | `ReactElement \| () => ReactElement` | Error fallback for the route. Overrides the global `defaultErrorElement` set in `Router` |
55
- | `staleTime` | `number` | Time in ms before cached data is considered stale and re-fetched in the background. If not provided, data never expires (cached forever) |
56
- | `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 refresh loader data using the router-provided `invalidate`. |
57
-
58
41
  ### `Router`
59
42
 
60
43
  | Prop | Type | Default | Description |
@@ -64,6 +47,7 @@ Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic pa
64
47
  | `animationDuration` | `number` | `optional` | Animation duration in milliseconds (browser default is used if not set) |
65
48
  | `defaultLoaderFallback` | `ReactElement \| () => ReactElement` | `optional` | Default loading fallback for every route loader |
66
49
  | `defaultErrorElement` | `ReactElement \| () => ReactElement` | `optional` | Default error fallback for every route |
50
+ | `defaultRetry` | `number \| { count: number; delay: number }` | `optional` | Default cache revalidation retry policy for all routes |
67
51
  | `beforeLoad` | `({ params, context, redirect, setContext }) => Promise<unknown> \| undefined \| void` | `undefined` | Runs before every navigation. Useful for authentication, analytics, or updating shared context. |
68
52
  | `afterLoad` | `({ params, context, setContext }) => Promise<void>` | `undefined` | Runs after every successful navigation. Useful for analytics, page tracking, or other global side effects. |
69
53
  | `spinner` | `boolean \| undefined` | `true` | Show a small spinner in the corner while loading data (only when `isAnimated` is enabled) |
@@ -82,9 +66,26 @@ Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic pa
82
66
  <Router routes={routes} spinner={false} isAnimated /> {/* disable the spinner */}
83
67
  </div>
84
68
  ```
85
-
86
69
  > **Note:** When `isAnimated` is enabled, `loaderFallback` is not shown. Instead, a small spinner appears (if `spinner={true}`). On the initial page load, however, the route's loaderFallback is rendered if available.
87
70
 
71
+ ### `createRouter(routes)`
72
+
73
+ Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic params, builds nested paths.
74
+
75
+ | Property | Type | Description |
76
+ |----------|------|-------------|
77
+ | `path` | `string` | Route path, e.g., `/user/:userId` |
78
+ | `element` | `ReactElement \| () => ReactElement \| LazyComponent` | Component to render |
79
+ | `beforeLoad` | `({ params, context, redirect, setContext }) => Promise<unknown> \| undefined \| void` | Auth checks and redirects. Can update context via `setContext`. `redirect` is provided by the router |
80
+ | `loader` | `({ params, context, setContext }) => Promise<unknown>` | Fetch data using route params and context. Can update context via `setContext` |
81
+ | `afterLoad` | `({ params, context, setContext }) => Promise<void>` | Analytics, side effects after data is loaded. Can update context via `setContext` |
82
+ | `fallback` | `ReactElement \| () => ReactElement` | Loading fallback (for lazy loading) |
83
+ | `loaderFallback` | `ReactElement \| () => ReactElement` | Loading fallback for the route's `loader`. Overrides the global `defaultLoaderFallback` set in `Router` |
84
+ | `retry` | `number \| { count: number; delay: number }` | `optional` | Overrides the global cache revalidation retry policy for this route |
85
+ | `errorElement` | `ReactElement \| () => ReactElement` | Error fallback for the route. Overrides the global `defaultErrorElement` set in `Router` |
86
+ | `staleTime` | `number` | Time in ms before cached data is considered stale and re-fetched in the background. If not provided, data never expires (cached forever) |
87
+ | `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 refresh loader data using the router-provided `invalidate`. |
88
+
88
89
  ### `Link`
89
90
 
90
91
  Component for client-side navigation with prefetch support.
@@ -125,6 +126,68 @@ import { Router, Link } from 'clear-react-router';
125
126
  ```
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.
127
128
 
129
+ ## Retry
130
+
131
+ Sometimes a request may fail because of a temporary network issue or a short-lived server problem. Instead of immediately rendering the error state, you can configure the router to automatically retry loading route data.
132
+
133
+ ### Route-level retry
134
+
135
+ ```tsx
136
+ {
137
+ path: '/posts',
138
+ loader: loadPosts,
139
+ retry: 3,
140
+ }
141
+ ```
142
+
143
+ `retry: 3` means the router will make up to **3 additional attempts** after the initial failed request (up to **4 attempts** in total).
144
+
145
+ You can also specify a delay between attempts:
146
+
147
+ ```tsx
148
+ {
149
+ path: '/posts',
150
+ loader: loadPosts,
151
+ retry: {
152
+ count: 3,
153
+ delay: 500,
154
+ },
155
+ }
156
+ ```
157
+
158
+ ### Global retry
159
+
160
+ To apply the same retry policy to all routes, use `defaultRetry`:
161
+
162
+ ```tsx
163
+ <Router routes={routes} defaultRetry={2} />
164
+ ```
165
+
166
+ or with a delay:
167
+
168
+ ```tsx
169
+ <Router routes={routes} defaultRetry={{ count: 2, delay: 500 }} />
170
+ ```
171
+
172
+ A route-level `retry` always overrides `defaultRetry`.
173
+
174
+ ### How it works
175
+
176
+ Unlike many routing libraries, retry is **not limited to the initial loader execution**.
177
+
178
+ The retry policy is applied to the router's **cache revalidation mechanism**, so it automatically works for every operation that reloads route data, including:
179
+
180
+ * Initial route loading
181
+ * Cache revalidation
182
+ * `invalidate()`
183
+ * `prefetch()`
184
+
185
+ This ensures consistent behavior regardless of how the data is being refreshed.
186
+
187
+ ### Why?
188
+
189
+ The router treats the route loader as the single source of truth for route data. Since every data refresh goes through the same cache revalidation pipeline, retry is configured once and automatically applies everywhere without any additional code.
190
+
128
191
  ### `redirect`
129
192
 
130
193
  Function provided to `beforeLoad` for programmatic redirection.
@@ -1,2 +1,2 @@
1
1
  import { RouterProps } from '../types';
2
- export declare const Router: ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated, spinner, preserveScroll, showFallbackOnAnimation, prefetch, hoverPrefetchDelay, errorBoundary: ErrorBoundary, context: initialContext, defaultLoaderFallback, defaultErrorElement, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
2
+ export declare const Router: ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated, spinner, preserveScroll, showFallbackOnAnimation, prefetch, hoverPrefetchDelay, errorBoundary: ErrorBoundary, context: initialContext, defaultLoaderFallback, defaultErrorElement, defaultRetry, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -7,6 +7,7 @@ declare class RouterConfig {
7
7
  hoverPrefetchDelay: number;
8
8
  beforeLoad?: ClientRouteItem['beforeLoad'];
9
9
  afterLoad?: ClientRouteItem['afterLoad'];
10
+ defaultRetry?: RouterProps['defaultRetry'];
10
11
  configure(config: Partial<RouterConfig>): void;
11
12
  }
12
13
  export declare const routerConfig: RouterConfig;
@@ -1,6 +1 @@
1
- type Options = Partial<{
2
- onSuccess: (args: unknown) => void;
3
- onError: (args: unknown) => void;
4
- }> | undefined;
5
- export declare const useAction: (action: string, options?: Options) => (formData: FormData) => Promise<void>;
6
- export {};
1
+ export declare const useAction: (action: string, options?: import("../types").Options) => (arg: FormData) => Promise<void>;
@@ -1,2 +1 @@
1
- import type { Location } from '../types';
2
- export declare const useNavigate: () => (arg: Location | string | -1) => Promise<void>;
1
+ export declare const useNavigate: () => (arg: import("..").Location | string | -1) => Promise<void>;
package/dist/index.js CHANGED
@@ -95,6 +95,7 @@ var RouterConfig = class {
95
95
  _defineProperty(this, "hoverPrefetchDelay", 150);
96
96
  _defineProperty(this, "beforeLoad", void 0);
97
97
  _defineProperty(this, "afterLoad", void 0);
98
+ _defineProperty(this, "defaultRetry", void 0);
98
99
  }
99
100
  configure(config) {
100
101
  Object.assign(this, config);
@@ -227,9 +228,9 @@ var findRoute = (pathname, includeAll) => {
227
228
  //#endregion
228
229
  //#region runtime/navigate.ts
229
230
  var navigationSeq = 0;
230
- var createNavigate = (routerSta, revalidateCache) => {
231
- const { loaderStateRef, scrollMapState, prevPathnameRef, loaderFallbackState, isLoadingState, contextState, timestampMap } = routerSta;
232
- const commitNavigation = createCommitNavigation(createCommitState(routerSta), prevPathnameRef);
231
+ var createNavigate = (routerState, revalidateCache) => {
232
+ const { loaderStateRef, scrollMapState, prevPathnameRef, loaderFallbackState, isLoadingState, contextState, timestampMap } = routerState;
233
+ const commitNavigation = createCommitNavigation(createCommitState(routerState), prevPathnameRef);
233
234
  const isCacheItemFresh = createIsCacheItemFresh(timestampMap);
234
235
  const getContext = () => ({
235
236
  context: contextState.getState(),
@@ -367,12 +368,11 @@ var createInvalidate = ({ routeItemDataState, loaderStateRef, timestampMap, curr
367
368
  await Promise.all(childPathList.map((el) => invalidateItem(`${pathname}${el}`, true)));
368
369
  }
369
370
  };
370
- async function invalidate(pathList, options) {
371
+ return async (pathList, options) => {
371
372
  const routePathname = routeItemDataState.getState().location.pathname;
372
373
  const pathnameList = Array.isArray(pathList) ? pathList : pathList ? [pathList] : [routePathname];
373
374
  await Promise.all(pathnameList.map((pathname) => invalidateItem(pathname, options?.withChildren)));
374
- }
375
- return invalidate;
375
+ };
376
376
  };
377
377
  //#endregion
378
378
  //#region runtime/prefetch.ts
@@ -387,51 +387,80 @@ var createPrefetch = (revalidateCache) => async (pathname) => {
387
387
  //#region utils/revalidateCache.ts
388
388
  var loaderMapRef = {};
389
389
  var loadingPromises = /* @__PURE__ */ new Map();
390
- var createRevalidateCache = (routerState) => ({ routeItem, pathname }) => {
391
- if (!routeItem?.loader) return;
392
- const isCacheItemFresh = createIsCacheItemFresh(routerState.timestampMap);
393
- const { loaderStateRef, timestampMap, contextState } = routerState;
394
- if (loadingPromises.has(pathname)) return loadingPromises.get(pathname);
395
- if (isCacheItemFresh({
396
- routeItem,
397
- pathname
398
- })) {
399
- loaderStateRef.set(loaderMapRef[pathname]);
400
- return;
401
- }
402
- const promise = (async () => {
390
+ var isObjectRetry = (arg) => typeof arg === "object";
391
+ var createRetry = (arg) => {
392
+ if (arg === void 0) return null;
393
+ return {
394
+ count: isObjectRetry(arg) ? arg.count : arg,
395
+ delay: isObjectRetry(arg) ? arg.delay : 0
396
+ };
397
+ };
398
+ var getRetry = (routeItem) => {
399
+ const routeRetry = createRetry(routeItem?.retry);
400
+ const globalRetry = createRetry(routerConfig.defaultRetry);
401
+ if (!routeRetry && !globalRetry) return null;
402
+ return {
403
+ count: routeRetry ? routeRetry.count : globalRetry?.count || 0,
404
+ delay: routeRetry ? routeRetry.delay : globalRetry?.delay || 0
405
+ };
406
+ };
407
+ var sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
408
+ var createRevalidateCache = (routerState) => {
409
+ const revalidateCache = async ({ routeItem, pathname }, retried = 0) => {
403
410
  if (!routeItem?.loader) return;
404
- try {
405
- const context = contextState.getState();
406
- const setContext = contextState.setState;
407
- const params = getParamsObject({
408
- params: routeItem.params,
409
- pathname
410
- });
411
- const result = await routeItem?.loader({
412
- params,
413
- context,
414
- setContext
415
- });
416
- timestampMap.set(pathname, Date.now());
417
- loaderStateRef.set((prev) => ({
418
- ...prev,
419
- data: result,
420
- loaderError: null
421
- }));
422
- loaderMapRef[pathname] = loaderStateRef.value;
423
- } catch (error) {
424
- loaderStateRef.set((prev) => ({
425
- ...prev,
426
- data: null,
427
- loaderError: error
428
- }));
429
- } finally {
430
- loadingPromises.delete(pathname);
411
+ const isCacheItemFresh = createIsCacheItemFresh(routerState.timestampMap);
412
+ const { loaderStateRef, timestampMap, contextState } = routerState;
413
+ if (loadingPromises.has(pathname)) return loadingPromises.get(pathname);
414
+ if (isCacheItemFresh({
415
+ routeItem,
416
+ pathname
417
+ })) {
418
+ loaderStateRef.set(loaderMapRef[pathname]);
419
+ return;
431
420
  }
432
- })();
433
- loadingPromises.set(pathname, promise);
434
- return promise;
421
+ const promise = (async () => {
422
+ if (!routeItem?.loader) return;
423
+ try {
424
+ const context = contextState.getState();
425
+ const setContext = contextState.setState;
426
+ const params = getParamsObject({
427
+ params: routeItem.params,
428
+ pathname
429
+ });
430
+ const result = await routeItem?.loader({
431
+ params,
432
+ context,
433
+ setContext
434
+ });
435
+ timestampMap.set(pathname, Date.now());
436
+ loaderStateRef.set((prev) => ({
437
+ ...prev,
438
+ data: result,
439
+ loaderError: null
440
+ }));
441
+ loaderMapRef[pathname] = loaderStateRef.value;
442
+ } catch (error) {
443
+ const retry = getRetry(routeItem);
444
+ if (retry && retry.count > retried) {
445
+ loadingPromises.delete(pathname);
446
+ if (retry.delay) await sleep(retry.delay);
447
+ await revalidateCache({
448
+ routeItem,
449
+ pathname
450
+ }, retried + 1);
451
+ } else loaderStateRef.set((prev) => ({
452
+ ...prev,
453
+ data: null,
454
+ loaderError: error
455
+ }));
456
+ } finally {
457
+ loadingPromises.delete(pathname);
458
+ }
459
+ })();
460
+ loadingPromises.set(pathname, promise);
461
+ return promise;
462
+ };
463
+ return revalidateCache;
435
464
  };
436
465
  //#endregion
437
466
  //#region cell.ts
@@ -471,6 +500,31 @@ var createRouterInstance = () => {
471
500
  timestampMap: /* @__PURE__ */ new Map()
472
501
  };
473
502
  const revalidateCache = createRevalidateCache(routerState);
503
+ const navigate = createNavigate(routerState, revalidateCache);
504
+ const invalidate = createInvalidate(routerState, revalidateCache);
505
+ const prefetch = createPrefetch(revalidateCache);
506
+ const useGetAction = (actionKey) => {
507
+ const { routeItem, location } = routerState.routeItemDataState.getState();
508
+ const context = routerState.contextState.getState();
509
+ const setContext = routerState.contextState.setState;
510
+ const params = getParamsObject({
511
+ params: routeItem?.params,
512
+ pathname: location.pathname
513
+ });
514
+ if (!routeItem) throw new Error("Route not found");
515
+ if (!routeItem.actions) throw new Error("Route action creator not found");
516
+ const action = routeItem.actions({
517
+ context,
518
+ setContext,
519
+ params,
520
+ invalidate
521
+ })[actionKey];
522
+ if (!action) throw new Error(`Action "${actionKey}" not found`);
523
+ return {
524
+ currentAction: action,
525
+ invalidate
526
+ };
527
+ };
474
528
  return {
475
529
  state: {
476
530
  isLoadingState: routerState.isLoadingState,
@@ -483,9 +537,9 @@ var createRouterInstance = () => {
483
537
  prevPathnameRef: routerState.prevPathnameRef
484
538
  },
485
539
  runtime: {
486
- navigate: createNavigate(routerState, revalidateCache),
487
- invalidate: createInvalidate(routerState, revalidateCache),
488
- prefetch: createPrefetch(revalidateCache)
540
+ navigate,
541
+ invalidate,
542
+ prefetch
489
543
  },
490
544
  hooks: {
491
545
  useIsLoading: () => useGlobalState(routerState.isLoadingState),
@@ -494,7 +548,55 @@ var createRouterInstance = () => {
494
548
  useRouteItemData: () => useGlobalState(routerState.routeItemDataState),
495
549
  useCurrentLoaderState: () => useGlobalState(routerState.currentLoaderState),
496
550
  useScrollMap: () => useGlobalState(routerState.scrollMapState),
497
- useContextState: () => useGlobalState(routerState.contextState)
551
+ useContextState: () => useGlobalState(routerState.contextState),
552
+ useParams: () => {
553
+ const routeItemData = routerState.routeItemDataState.getState();
554
+ return getParamsObject({
555
+ params: routeItemData.routeItem?.params,
556
+ pathname: routeItemData.location.pathname
557
+ });
558
+ },
559
+ useNavigate: () => {
560
+ const { blockedRouteState } = routerState;
561
+ const { location } = routerState.routeItemDataState.getState();
562
+ return async (arg) => {
563
+ if (arg !== -1 && blockedRouteState.getState().from) {
564
+ const to = typeof arg === "object" ? arg.pathname : arg;
565
+ blockedRouteState.setState((prevState) => ({
566
+ ...prevState,
567
+ to
568
+ }));
569
+ return;
570
+ }
571
+ if (arg === -1) return history.go(arg);
572
+ if (typeof arg === "string") {
573
+ if (arg !== location.pathname) await navigate({ pathname: arg });
574
+ } else if (JSON.stringify(arg) !== JSON.stringify(location)) await navigate(arg);
575
+ };
576
+ },
577
+ useRestoreScroll: () => {
578
+ const { pathname } = routerState.routeItemDataState.getState().location;
579
+ const scrollMap = routerState.scrollMapState.getState();
580
+ return () => {
581
+ if (scrollMap[pathname]) requestAnimationFrame(() => window.scrollTo({
582
+ top: scrollMap[pathname],
583
+ behavior: "smooth"
584
+ }));
585
+ };
586
+ },
587
+ useGetAction,
588
+ useAction: (action, options = {}) => {
589
+ const { currentAction, invalidate } = useGetAction(action);
590
+ return async (formData) => {
591
+ try {
592
+ const result = await currentAction(formData);
593
+ await invalidate();
594
+ options.onSuccess?.(result);
595
+ } catch (error) {
596
+ options.onError?.(error);
597
+ }
598
+ };
599
+ }
498
600
  }
499
601
  };
500
602
  };
@@ -567,21 +669,16 @@ var useApplyCustomAnimation = (animationDuration) => {
567
669
  }, [animationDuration]);
568
670
  };
569
671
  //#endregion
672
+ //#region hooks/useLocation.ts
673
+ var useLocation = () => {
674
+ const [routeItemData] = router.hooks.useRouteItemData();
675
+ return routeItemData.location;
676
+ };
677
+ //#endregion
570
678
  //#region hooks/usePreserveScroll.ts
571
679
  var usePreserveScroll = (preserveScroll) => {
572
- const { useRouteItemData, useScrollMap } = router.hooks;
573
- const [routeItemData] = useRouteItemData();
574
- const [scrollMap] = useScrollMap();
575
- const { pathname } = routeItemData.location;
576
- const restoreScroll = useCallback(() => {
577
- if (!pathname || !scrollMap[pathname]) return;
578
- requestAnimationFrame(() => {
579
- window.scrollTo({
580
- top: scrollMap[pathname],
581
- behavior: "smooth"
582
- });
583
- });
584
- }, [pathname, scrollMap]);
680
+ const restoreScroll = router.hooks.useRestoreScroll();
681
+ const { pathname } = useLocation();
585
682
  useEffect(() => {
586
683
  if (preserveScroll) restoreScroll();
587
684
  }, [
@@ -615,7 +712,7 @@ var renderElement = (Component) => {
615
712
  //#endregion
616
713
  //#region components/Router.tsx
617
714
  var EmptyBoundary = ({ children }) => children;
618
- var Router = ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated = false, spinner = true, preserveScroll = true, showFallbackOnAnimation = false, prefetch = "hover", hoverPrefetchDelay = 150, errorBoundary: ErrorBoundary = EmptyBoundary, context: initialContext, defaultLoaderFallback, defaultErrorElement }) => {
715
+ var Router = ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated = false, spinner = true, preserveScroll = true, showFallbackOnAnimation = false, prefetch = "hover", hoverPrefetchDelay = 150, errorBoundary: ErrorBoundary = EmptyBoundary, context: initialContext, defaultLoaderFallback, defaultErrorElement, defaultRetry }) => {
619
716
  const { useIsLoading, useLoaderFallback, useRouteItemData, useCurrentLoaderState } = router.hooks;
620
717
  const [isLoading] = useIsLoading();
621
718
  const [currentLoaderFallback] = useLoaderFallback();
@@ -629,7 +726,8 @@ var Router = ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated = f
629
726
  hoverPrefetchDelay,
630
727
  showFallbackOnAnimation,
631
728
  beforeLoad,
632
- afterLoad
729
+ afterLoad,
730
+ defaultRetry
633
731
  });
634
732
  useApplyCustomAnimation(animationDuration);
635
733
  useSetInitialContext(initialContext);
@@ -648,45 +746,8 @@ var Router = ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated = f
648
746
  });
649
747
  };
650
748
  //#endregion
651
- //#region hooks/useLocation.ts
652
- var useLocation = () => {
653
- const [routeItemData] = router.hooks.useRouteItemData();
654
- return routeItemData.location;
655
- };
656
- //#endregion
657
- //#region hooks/useLatest.ts
658
- var useLatest = (value) => {
659
- const ref = useRef(value);
660
- useEffect(() => {
661
- ref.current = value;
662
- }, [value]);
663
- return ref;
664
- };
665
- //#endregion
666
749
  //#region hooks/useNavigate.ts
667
- var useNavigate = () => {
668
- const [blockedRoute, setBlockedRoute] = router.hooks.useBlockedRoute();
669
- const locationRef = useLatest(useLocation());
670
- const blockedRouteRef = useLatest(blockedRoute);
671
- return useCallback(async (arg) => {
672
- if (arg !== -1 && blockedRouteRef.current.from) {
673
- const to = typeof arg === "object" ? arg.pathname : arg;
674
- setBlockedRoute((prevState) => ({
675
- ...prevState,
676
- to
677
- }));
678
- return;
679
- }
680
- if (arg === -1) return history.go(arg);
681
- if (typeof arg === "string") {
682
- if (arg !== locationRef.current.pathname) await router.runtime.navigate({ pathname: arg });
683
- } else if (JSON.stringify(arg) !== JSON.stringify(locationRef.current)) await router.runtime.navigate(arg);
684
- }, [
685
- blockedRouteRef,
686
- locationRef,
687
- setBlockedRoute
688
- ]);
689
- };
750
+ var useNavigate = router.hooks.useNavigate;
690
751
  //#endregion
691
752
  //#region components/Link.tsx
692
753
  var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }) => {
@@ -749,46 +810,9 @@ var FormProvider = ({ children, isSubmitting }) => /* @__PURE__ */ (0, import_js
749
810
  children
750
811
  });
751
812
  //#endregion
752
- //#region hooks/useInvalidate.ts
753
- var useInvalidate = () => router.runtime.invalidate;
754
- //#endregion
755
- //#region hooks/useParams.ts
756
- var useParams = () => {
757
- const [routeItemData] = router.hooks.useRouteItemData();
758
- const { routeItem, location: { pathname } } = routeItemData;
759
- return useMemo(() => routeItem ? getParamsObject({
760
- params: routeItem?.params,
761
- pathname
762
- }) : void 0, [pathname, routeItem]);
763
- };
764
- //#endregion
765
- //#region hooks/useGetAction.ts
766
- var useGetAction = (actionKey) => {
767
- const { useRouteItemData, useContextState } = router.hooks;
768
- const invalidate = useInvalidate();
769
- const [routeItemData] = useRouteItemData();
770
- const [context, setContext] = useContextState();
771
- const params = useParams();
772
- const { routeItem } = routeItemData;
773
- const latestContext = useLatest(context);
774
- if (!routeItem) throw new Error("Route not found");
775
- if (!routeItem.actions) throw new Error("Route action creator not found");
776
- const action = routeItem.actions({
777
- context: latestContext.current,
778
- setContext,
779
- params,
780
- invalidate
781
- })[actionKey];
782
- if (!action) throw new Error(`Action "${actionKey}" not found`);
783
- return {
784
- currentAction: action,
785
- invalidate
786
- };
787
- };
788
- //#endregion
789
813
  //#region components/Form.tsx
790
814
  var Form = ({ children, action, onSuccess, onError, autoReset = true }) => {
791
- const { currentAction, invalidate } = useGetAction(action);
815
+ const { currentAction, invalidate } = router.hooks.useGetAction(action);
792
816
  const [isSubmitting, setIsSubmitting] = useState(false);
793
817
  const onSubmit = async (evt) => {
794
818
  evt.preventDefault();
@@ -813,12 +837,18 @@ var Form = ({ children, action, onSuccess, onError, autoReset = true }) => {
813
837
  });
814
838
  };
815
839
  //#endregion
840
+ //#region hooks/useParams.ts
841
+ var useParams = router.hooks.useParams;
842
+ //#endregion
816
843
  //#region hooks/useLoaderState.ts
817
844
  var useLoaderState = () => {
818
845
  const [loaderState] = router.hooks.useCurrentLoaderState();
819
846
  return loaderState;
820
847
  };
821
848
  //#endregion
849
+ //#region hooks/useInvalidate.ts
850
+ var useInvalidate = () => router.runtime.invalidate;
851
+ //#endregion
822
852
  //#region hooks/useBlocker.ts
823
853
  var useBlocker = (blockerFn) => {
824
854
  const { hooks: { useBlockedRoute, useRouteItemData }, runtime: { navigate } } = router;
@@ -864,25 +894,7 @@ var useBlocker = (blockerFn) => {
864
894
  };
865
895
  //#endregion
866
896
  //#region hooks/useAction.ts
867
- var useAction = (action, options = {}) => {
868
- const { currentAction, invalidate } = useGetAction(action);
869
- const latestOnSuccess = useLatest(options?.onSuccess);
870
- const latestOnError = useLatest(options?.onError);
871
- return useCallback(async (formData) => {
872
- try {
873
- const result = await currentAction(formData);
874
- await invalidate();
875
- latestOnSuccess.current?.(result);
876
- } catch (error) {
877
- latestOnError.current?.(error);
878
- }
879
- }, [
880
- currentAction,
881
- invalidate,
882
- latestOnError,
883
- latestOnSuccess
884
- ]);
885
- };
897
+ var useAction = router.hooks.useAction;
886
898
  //#endregion
887
899
  //#region hooks/useRouterContext.ts
888
900
  var useRouterContext = () => {
@@ -922,6 +934,15 @@ var useSearch = () => {
922
934
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
923
935
  };
924
936
  //#endregion
937
+ //#region hooks/useLatest.ts
938
+ var useLatest = (value) => {
939
+ const ref = useRef(value);
940
+ useEffect(() => {
941
+ ref.current = value;
942
+ }, [value]);
943
+ return ref;
944
+ };
945
+ //#endregion
925
946
  //#region hooks/useSearchParams.ts
926
947
  var useSearchParams = () => {
927
948
  const search = useSearch();
@@ -1,5 +1,2 @@
1
1
  import { type InvalidateOptions, RevalidateCache, RouterState } from '../types';
2
- export declare const createInvalidate: ({ routeItemDataState, loaderStateRef, timestampMap, currentLoaderState, contextState }: RouterState, revalidateCache: RevalidateCache) => {
3
- (pathList?: string[]): Promise<void>;
4
- (path?: string, options?: InvalidateOptions): Promise<void>;
5
- };
2
+ export declare const createInvalidate: ({ routeItemDataState, loaderStateRef, timestampMap, currentLoaderState, contextState }: RouterState, revalidateCache: RevalidateCache) => (pathList?: string | string[], options?: InvalidateOptions) => Promise<void>;
@@ -1,2 +1,2 @@
1
1
  import { Location, RevalidateCache, RouterState } from '../types';
2
- export declare const createNavigate: (routerSta: RouterState, revalidateCache: RevalidateCache) => (nextLocation: Location) => Promise<void>;
2
+ export declare const createNavigate: (routerState: RouterState, revalidateCache: RevalidateCache) => (nextLocation: Location) => Promise<void>;
package/dist/types.d.ts CHANGED
@@ -24,6 +24,7 @@ export type ClientRouteItem = {
24
24
  fallback?: RenderElement;
25
25
  children?: ClientRouteItem[];
26
26
  staleTime?: number;
27
+ retry?: Retry;
27
28
  beforeLoad?: BeforeLoad;
28
29
  afterLoad?: (arg: {
29
30
  context: Record<string, unknown>;
@@ -68,12 +69,18 @@ export type RouteItemData = {
68
69
  location: Location;
69
70
  routeItem: RouteItem | undefined;
70
71
  };
72
+ type ObjectRetry = {
73
+ count: number;
74
+ delay: number;
75
+ };
76
+ export type Retry = number | ObjectRetry | undefined;
71
77
  export type RouterProps = {
72
78
  routes: RouteItem[];
73
79
  isAnimated?: boolean;
74
80
  animationDuration?: number;
75
81
  spinner?: boolean;
76
82
  preserveScroll?: boolean;
83
+ defaultRetry?: Retry;
77
84
  defaultLoaderFallback?: RenderElement;
78
85
  defaultErrorElement?: RenderElement;
79
86
  showFallbackOnAnimation?: boolean;
@@ -116,12 +123,25 @@ export type RouterType = {
116
123
  }>>;
117
124
  useLoaderFallback: () => ReturnType<typeof useGlobalState<RenderElement | undefined>>;
118
125
  useRouteItemData: () => ReturnType<typeof useGlobalState<RouteItemData>>;
119
- useCurrentLoaderState: () => ReturnType<typeof useGlobalState<LoaderState<unknown>>>;
126
+ useCurrentLoaderState: () => ReturnType<typeof useGlobalState<LoaderState>>;
120
127
  useScrollMap: () => ReturnType<typeof useGlobalState<Record<string, number>>>;
121
128
  useContextState: () => ReturnType<typeof useGlobalState<Record<string, unknown>>>;
129
+ useParams: <T>() => T;
130
+ useNavigate: () => (arg: Location | string | -1) => Promise<void>;
131
+ useGetAction: (actionKey: string) => {
132
+ currentAction: (arg: FormData) => Promise<unknown> | Promise<void> | void | unknown;
133
+ invalidate: (pathList?: string | string[], options?: InvalidateOptions) => Promise<void>;
134
+ };
135
+ useRestoreScroll: () => () => void;
136
+ useAction: (action: string, options?: Options) => (arg: FormData) => Promise<void>;
122
137
  };
123
138
  };
124
139
  export type InvalidateOptions = {
125
140
  withChildren?: boolean;
126
141
  };
127
142
  export type RevalidateCache = ({ routeItem, pathname }: RevalidateCacheArgs) => Promise<unknown> | undefined;
143
+ export type Options = Partial<{
144
+ onSuccess: (args: unknown) => void;
145
+ onError: (args: unknown) => void;
146
+ }> | undefined;
147
+ export {};
@@ -1,2 +1,2 @@
1
1
  import { RevalidateCacheArgs, RouterState } from '../types';
2
- export declare const createRevalidateCache: (routerState: RouterState) => ({ routeItem, pathname }: RevalidateCacheArgs) => any;
2
+ export declare const createRevalidateCache: (routerState: RouterState) => ({ routeItem, pathname }: RevalidateCacheArgs, retried?: number) => Promise<any>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {
@@ -1,4 +0,0 @@
1
- export declare const useGetAction: (actionKey: string) => {
2
- currentAction: (arg: FormData) => Promise<unknown> | Promise<void> | void | unknown;
3
- invalidate: (pathList?: string | string[], options?: import("../types").InvalidateOptions) => Promise<void>;
4
- };