clear-react-router 1.8.8 → 1.9.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 CHANGED
@@ -48,10 +48,11 @@ It provides first-class support for:
48
48
  | `defaultLoaderFallback` | `ReactElement \| () => ReactElement` | `optional` | Default loading fallback for every route loader |
49
49
  | `defaultErrorElement` | `ReactElement \| () => ReactElement` | `optional` | Default error fallback for every route |
50
50
  | `defaultRetry` | `number \| { count: number; delay: number }` | `optional` | Default cache revalidation retry policy for all routes |
51
+ | `defaultStaleTime` | `number` | `optional` | Default time in milliseconds before cached loader data is considered stale |
51
52
  | `beforeLoad` | `({ params, context, redirect, setContext }) => Promise<unknown> \| undefined \| void` | `undefined` | Runs before every navigation. Useful for authentication, analytics, or updating shared context. |
52
53
  | `afterLoad` | `({ params, context, setContext }) => Promise<void>` | `undefined` | Runs after every successful navigation. Useful for analytics, page tracking, or other global side effects. |
53
54
  | `spinner` | `boolean \| undefined` | `true` | Show a small spinner in the corner while loading data (only when `isAnimated` is enabled) |
54
- | `preserveScroll` | `boolean \| undefined` | `true` | Save and restore scroll position when navigating between pages |
55
+ | `defaultPreserveScroll` | `boolean \| undefined` | `true` | Default value for save and restore scroll position when navigating between pages |
55
56
  | `showFallbackOnAnimation` | `boolean \| undefined` | `false` | Show `loaderFallback` even when `isAnimated` is `true` (instead of spinner) |
56
57
  | `prefetch` | `'hover' \| 'render' \| 'viewport' \| 'none'` | `'hover'` | Default prefetch strategy for all `<Link>` components |
57
58
  | `hoverPrefetchDelay` | `number` | `150` | Delay in milliseconds before prefetching on hover (only for `'hover'` strategy) |
@@ -76,15 +77,27 @@ Normalizes route configuration. Extracts dynamic params, builds nested paths.
76
77
  |----------|------|-------------|
77
78
  | `path` | `string` | Route path, e.g., `/user/:userId` |
78
79
  | `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` |
80
+ | `beforeLoad` | `({ params, context, redirect, setContext }) => Promise<unknown> \| undefined \| void` | Runs before every route navigation. Auth checks and redirects. Can update context via `setContext`. `redirect` is provided by the router |
81
+ | `loader` | `({ params, context, setContext, searchParams }) => Promise<unknown>` | Fetch data using route params, search params, and context. Can update context via `setContext` |
82
+ | `afterLoad` | `({ params, context, setContext }) => Promise<void>` | Runs after a successful navigation once the route has finished loading. Analytics, side effects after data is loaded. Can update context via `setContext` |
82
83
  | `fallback` | `ReactElement \| () => ReactElement` | Loading fallback (for lazy loading) |
83
84
  | `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
+ | `retry` | `number \| { count: number; delay: number }` | `undefined` | Overrides the global cache revalidation retry policy for this route |
85
86
  | `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`. |
87
+ | `staleTime` | `number` | Time in milliseconds before cached loader data is considered stale. Overrides Router.defaultStaleTime. If neither value is provided, cached data never expires |
88
+ | `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 invalidate cached loader data using the router-provided `invalidate` |
89
+ | `pollingInterval` | `number \| undefined` | `undefined` | Polling interval (in milliseconds) for automatically revalidating data while the route is active |
90
+ | `preserveScroll` | `boolean \| undefined` | `undefined` | Save and restore route scroll position when navigating between pages |
91
+
92
+ Loader arguments:
93
+ ```ts
94
+ {
95
+ params: Record<string, string>; // Route parameters
96
+ context: Record<string, unknown>; // Router context
97
+ setContext: Dispatch<SetStateAction<Record<string, unknown>>>; // Updates the router context
98
+ searchParams: Record<string, string>; // URL search parameters
99
+ }
100
+ ```
88
101
 
89
102
  ### `Link`
90
103
 
@@ -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, defaultRetry, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
2
+ export declare const Router: ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated, spinner, defaultPreserveScroll, showFallbackOnAnimation, prefetch, hoverPrefetchDelay, errorBoundary: ErrorBoundary, context: initialContext, defaultLoaderFallback, defaultErrorElement, defaultRetry, defaultStaleTime, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -8,6 +8,8 @@ declare class RouterConfig {
8
8
  beforeLoad?: ClientRouteItem['beforeLoad'];
9
9
  afterLoad?: ClientRouteItem['afterLoad'];
10
10
  defaultRetry?: RouterProps['defaultRetry'];
11
+ defaultStaleTime?: RouterProps['defaultStaleTime'];
12
+ defaultPreserveScroll?: RouterProps['defaultPreserveScroll'];
11
13
  configure(config: Partial<RouterConfig>): void;
12
14
  }
13
15
  export declare const routerConfig: RouterConfig;
@@ -1 +1,2 @@
1
- export declare const usePreserveScroll: (preserveScroll: boolean) => void;
1
+ import { RouteItemData } from '../types';
2
+ export declare const usePreserveScroll: ({ routeItem, location: { pathname } }: RouteItemData) => void;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Suspense, createContext, lazy, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
1
+ import { Suspense, createContext, lazy, useCallback, useContext, useEffect, useLayoutEffect, 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
@@ -96,6 +96,8 @@ var RouterConfig = class {
96
96
  _defineProperty(this, "beforeLoad", void 0);
97
97
  _defineProperty(this, "afterLoad", void 0);
98
98
  _defineProperty(this, "defaultRetry", void 0);
99
+ _defineProperty(this, "defaultStaleTime", void 0);
100
+ _defineProperty(this, "defaultPreserveScroll", void 0);
99
101
  }
100
102
  configure(config) {
101
103
  Object.assign(this, config);
@@ -120,10 +122,11 @@ var createCommitNavigation = (navigationExecutor, prevPathnameRef) => (nextLocat
120
122
  //#region utils/isCacheItemFresh.ts
121
123
  var createIsCacheItemFresh = (timestampMap) => ({ routeItem, pathname }) => {
122
124
  if (!routeItem) return true;
123
- const currentCacheTimestamp = timestampMap.get(pathname);
124
- if (!currentCacheTimestamp) return false;
125
- if (!routeItem.staleTime) return true;
126
- return Date.now() - currentCacheTimestamp < routeItem.staleTime;
125
+ const timestamp = timestampMap.get(pathname);
126
+ if (timestamp === void 0) return false;
127
+ const staleTime = routeItem.staleTime ?? routerConfig.defaultStaleTime;
128
+ if (staleTime === void 0) return true;
129
+ return Date.now() - timestamp <= staleTime;
127
130
  };
128
131
  //#endregion
129
132
  //#region ../../node_modules/react/cjs/react-jsx-runtime.production.js
@@ -218,8 +221,9 @@ var findRoute = (pathname, includeAll) => {
218
221
  //#endregion
219
222
  //#region runtime/navigate.ts
220
223
  var navigationSeq = 0;
224
+ var interval = 0;
221
225
  var createNavigate = (routerState, revalidateCache) => {
222
- const { loaderStateRef, scrollMapState, prevPathnameRef, loaderFallbackState, isLoadingState, contextState, timestampMap, pendingPathRef } = routerState;
226
+ const { loaderStateRef, scrollMapState, prevPathnameRef, loaderFallbackState, isLoadingState, contextState, timestampMap, pendingPathRef, routeItemDataState } = routerState;
223
227
  const commitNavigation = createCommitNavigation(createCommitState(routerState), prevPathnameRef);
224
228
  const isCacheItemFresh = createIsCacheItemFresh(timestampMap);
225
229
  const getContext = () => ({
@@ -273,15 +277,28 @@ var createNavigate = (routerState, revalidateCache) => {
273
277
  pathname: location.pathname
274
278
  }) || isAnimated && !showFallback ? void 0 : routeItem?.loaderFallback);
275
279
  };
280
+ const beforeEachLoad = (location) => {
281
+ window.clearInterval(interval);
282
+ if (routeItemDataState.getState().location.pathname !== location.pathname) isLoadingState.setState(true);
283
+ pendingPathRef.set(location.pathname);
284
+ };
285
+ const afterEachLoad = (routeItem) => {
286
+ pendingPathRef.set("");
287
+ if (!routeItem?.pollingInterval) return;
288
+ interval = window.setInterval(() => revalidateCache({
289
+ routeItem,
290
+ pathname: location.pathname
291
+ }), routeItem.pollingInterval);
292
+ };
276
293
  const loader = async (routeItem, location) => {
277
294
  if (!routeItem?.loader) return;
278
- isLoadingState.setState(true);
279
- pendingPathRef.set(location.pathname);
295
+ beforeEachLoad(location);
280
296
  await revalidateCache({
281
297
  routeItem,
282
- pathname: location.pathname
298
+ pathname: location.pathname,
299
+ search: location.search
283
300
  });
284
- pendingPathRef.set("");
301
+ afterEachLoad(routeItem);
285
302
  };
286
303
  const afterLoad = async (routeItem, params) => {
287
304
  const { afterLoad } = routerConfig;
@@ -399,7 +416,7 @@ var getRetry = (routeItem) => {
399
416
  var sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
400
417
  var createRevalidateCache = (routerState) => {
401
418
  const { loaderStateRef, timestampMap, contextState } = routerState;
402
- const revalidateCache = async ({ routeItem, pathname }, retried = 0) => {
419
+ const revalidateCache = async ({ routeItem, pathname, search = "" }, retried = 0) => {
403
420
  if (!routeItem?.loader) return;
404
421
  const isCacheItemFresh = createIsCacheItemFresh(timestampMap);
405
422
  if (loadingPromises.has(pathname)) return loadingPromises.get(pathname);
@@ -416,18 +433,20 @@ var createRevalidateCache = (routerState) => {
416
433
  const context = contextState.getState();
417
434
  const setContext = contextState.setState;
418
435
  const params = getParamsObject(routeItem, pathname);
436
+ const searchParams = Object.fromEntries(new URLSearchParams(search).entries());
419
437
  const result = await routeItem?.loader({
420
438
  params,
421
439
  context,
422
- setContext
440
+ setContext,
441
+ searchParams
423
442
  });
424
- timestampMap.set(pathname, Date.now());
443
+ timestampMap.set(`${pathname}${search}`, Date.now());
425
444
  loaderStateRef.set((prev) => ({
426
445
  ...prev,
427
446
  data: result,
428
447
  loaderError: null
429
448
  }));
430
- loaderMapRef[pathname] = loaderStateRef.value;
449
+ loaderMapRef[`${pathname}${search}`] = loaderStateRef.value;
431
450
  return {
432
451
  data: result,
433
452
  error: null
@@ -435,7 +454,7 @@ var createRevalidateCache = (routerState) => {
435
454
  } catch (error) {
436
455
  const retry = getRetry(routeItem);
437
456
  if (retry && retry.count > retried) {
438
- loadingPromises.delete(pathname);
457
+ loadingPromises.delete(`${pathname}${search}`);
439
458
  if (retry.delay) await sleep(retry.delay);
440
459
  await revalidateCache({
441
460
  routeItem,
@@ -457,10 +476,10 @@ var createRevalidateCache = (routerState) => {
457
476
  };
458
477
  }
459
478
  } finally {
460
- loadingPromises.delete(pathname);
479
+ loadingPromises.delete(`${pathname}${search}`);
461
480
  }
462
481
  })();
463
- loadingPromises.set(pathname, promise);
482
+ loadingPromises.set(`${pathname}${search}`, promise);
464
483
  return promise;
465
484
  };
466
485
  return revalidateCache;
@@ -504,8 +523,8 @@ var createRouterInstance = () => {
504
523
  timestampMap: /* @__PURE__ */ new Map()
505
524
  };
506
525
  const revalidateCache = createRevalidateCache(routerState);
507
- const navigate = createNavigate(routerState, revalidateCache);
508
526
  const invalidate = createInvalidate(routerState, revalidateCache);
527
+ const navigate = createNavigate(routerState, revalidateCache);
509
528
  const prefetch = createPrefetch(revalidateCache);
510
529
  const useGetAction = (actionKey) => {
511
530
  const { routeItem } = routerState.routeItemDataState.getState();
@@ -665,16 +684,10 @@ var useApplyCustomAnimation = (animationDuration) => {
665
684
  }, [animationDuration]);
666
685
  };
667
686
  //#endregion
668
- //#region hooks/useLocation.ts
669
- var useLocation = () => {
670
- const [routeItemData] = router.hooks.useRouteItemData();
671
- return routeItemData.location;
672
- };
673
- //#endregion
674
687
  //#region hooks/usePreserveScroll.ts
675
- var usePreserveScroll = (preserveScroll) => {
688
+ var usePreserveScroll = ({ routeItem, location: { pathname } }) => {
689
+ const preserveScroll = routeItem?.preserveScroll === void 0 ? routerConfig.defaultPreserveScroll : routeItem.preserveScroll;
676
690
  const restoreScroll = router.hooks.useRestoreScroll();
677
- const { pathname } = useLocation();
678
691
  useEffect(() => {
679
692
  if (preserveScroll) restoreScroll();
680
693
  }, [
@@ -686,7 +699,7 @@ var usePreserveScroll = (preserveScroll) => {
686
699
  //#endregion
687
700
  //#region hooks/useSetRouterConfig.ts
688
701
  var useSetRouterConfig = (routerProps) => {
689
- useEffect(() => routerConfig.configure(routerProps), [routerProps]);
702
+ useLayoutEffect(() => routerConfig.configure(routerProps), [routerProps]);
690
703
  };
691
704
  //#endregion
692
705
  //#region hooks/useSetInitialContext.ts
@@ -708,7 +721,7 @@ var renderElement = (Component) => {
708
721
  //#endregion
709
722
  //#region components/Router.tsx
710
723
  var EmptyBoundary = ({ children }) => children;
711
- 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 }) => {
724
+ var Router = ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated = false, spinner = true, defaultPreserveScroll = true, showFallbackOnAnimation = false, prefetch = "hover", hoverPrefetchDelay = 150, errorBoundary: ErrorBoundary = EmptyBoundary, context: initialContext, defaultLoaderFallback, defaultErrorElement, defaultRetry, defaultStaleTime }) => {
712
725
  const { useIsLoading, useLoaderFallback, useRouteItemData, useCurrentLoaderState } = router.hooks;
713
726
  const [isLoading] = useIsLoading();
714
727
  const [currentLoaderFallback] = useLoaderFallback();
@@ -723,15 +736,17 @@ var Router = ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated = f
723
736
  showFallbackOnAnimation,
724
737
  beforeLoad,
725
738
  afterLoad,
726
- defaultRetry
739
+ defaultRetry,
740
+ defaultStaleTime,
741
+ defaultPreserveScroll
727
742
  });
728
743
  useApplyCustomAnimation(animationDuration);
729
744
  useSetInitialContext(initialContext);
730
- usePreserveScroll(preserveScroll);
745
+ usePreserveScroll(routeItemData);
746
+ const { routeItem, location } = routeItemData;
731
747
  const showErrorElement = !isLoading && Boolean(loaderState.loaderError || loaderState.beforeLoadError);
732
748
  const showSpinner = spinner && isAnimated && isLoading;
733
749
  const loadingContent = !showErrorElement && isLoading;
734
- const { routeItem, location } = routeItemData;
735
750
  if ((showFallbackOnAnimation || !isAnimated) && loadingContent) return renderElement(currentLoaderFallback || defaultLoaderFallback);
736
751
  if (!showFallbackOnAnimation && isAnimated && loadingContent) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
737
752
  if (!routeItem) return null;
@@ -752,6 +767,12 @@ var useIsRoutePending = (routePath) => {
752
767
  //#region hooks/useNavigate.ts
753
768
  var useNavigate = router.hooks.useNavigate;
754
769
  //#endregion
770
+ //#region hooks/useLocation.ts
771
+ var useLocation = () => {
772
+ const [routeItemData] = router.hooks.useRouteItemData();
773
+ return routeItemData.location;
774
+ };
775
+ //#endregion
755
776
  //#region components/Link.tsx
756
777
  var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, className, style, onClick, activeClassName = "active-link", pendingClassName = "pending-link" }) => {
757
778
  const isPending = useIsRoutePending(to);
package/dist/types.d.ts CHANGED
@@ -18,13 +18,16 @@ export type ClientRouteItem = {
18
18
  params: Record<string, string>;
19
19
  context: Record<string, unknown>;
20
20
  setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
21
+ searchParams: Record<string, string>;
21
22
  }): Promise<unknown>;
22
23
  loaderFallback?: RenderElement;
23
24
  errorElement?: RenderElement;
24
25
  fallback?: RenderElement;
25
26
  children?: ClientRouteItem[];
26
27
  staleTime?: number;
28
+ pollingInterval?: number;
27
29
  retry?: Retry;
30
+ preserveScroll?: boolean;
28
31
  beforeLoad?: BeforeLoad;
29
32
  afterLoad?: (arg: {
30
33
  context: Record<string, unknown>;
@@ -55,6 +58,7 @@ export type BlockerState = 'blocked' | 'unblocked' | 'charged';
55
58
  export type RevalidateCacheArgs = {
56
59
  pathname: string;
57
60
  routeItem?: RouteItem;
61
+ search?: string;
58
62
  };
59
63
  export type LoaderState<T = unknown> = {
60
64
  data: T;
@@ -79,8 +83,9 @@ export type RouterProps = {
79
83
  isAnimated?: boolean;
80
84
  animationDuration?: number;
81
85
  spinner?: boolean;
82
- preserveScroll?: boolean;
86
+ defaultPreserveScroll?: boolean;
83
87
  defaultRetry?: Retry;
88
+ defaultStaleTime?: number;
84
89
  defaultLoaderFallback?: RenderElement;
85
90
  defaultErrorElement?: RenderElement;
86
91
  showFallbackOnAnimation?: boolean;
@@ -1,2 +1,2 @@
1
1
  import { RevalidateCacheArgs, RouterState } from '../types';
2
- export declare const createRevalidateCache: (routerState: RouterState) => ({ routeItem, pathname }: RevalidateCacheArgs, retried?: number) => Promise<any>;
2
+ export declare const createRevalidateCache: (routerState: RouterState) => ({ routeItem, pathname, search }: 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.8",
3
+ "version": "1.9.0",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {