clear-react-router 1.8.8 → 1.9.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
@@ -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
 
@@ -632,122 +645,6 @@ useEffect(() => {
632
645
  }, [state, process, reset]);
633
646
  ```
634
647
 
635
- ### `useQueryParam()`
636
-
637
- A flexible hook for working with typed query parameters. You provide an adapter object with `parse` and `serialize` functions, and it returns the parsed value and a setter.
638
-
639
- ```tsx
640
- import { useQueryParam, adapter } from 'clear-react-router';
641
-
642
- const ProductPage = () => {
643
- // String parameter
644
- const [brand, setBrand] = useQueryParam('brand', adapter.string, 'nike');
645
-
646
- // Number parameter
647
- const [page, setPage] = useQueryParam('page', adapter.integer, 1);
648
-
649
- // Date parameter
650
- const [date, setDate] = useQueryParam('date', adapter.date, new Date());
651
-
652
- // Array of numbers
653
- const [prices, setPrices] = useQueryParam('prices', adapter.floatArray);
654
-
655
- return (
656
- <div>
657
- <p>Brand: {brand}</p>
658
- <p>Page: {page}</p>
659
- <button onClick={() => setPage(page + 1)}>Next</button>
660
- </div>
661
- );
662
- }
663
- ```
664
- type Adapter<T> = {
665
- parse: (params: string[]) => T;
666
- serialize?: (params: T) => string | string[];
667
- }
668
-
669
- **Signature:** `useQueryParam<T>(field: string, adapter: Adapter<T>, defaultValue?: T): [T, (arg: T | null) => void]`
670
-
671
- | Argument | Type | Description |
672
- |----------|------|-------------|
673
- | `field` | `string` | The query parameter key (e.g., `'page'`, `'brand'`) |
674
- | `adapter` | `Adapter<T>` | Parser and optional serializer for params (serializer String is used in case of serializer not passed) |
675
- | `defaultValue` | `T` (optional) | Default value returned when the parameter is missing or empty |
676
-
677
- **Returns:**
678
-
679
- | Element | Type | Description |
680
- |---------|------|-------------|
681
- | `value` | `T` | The parsed value from the query parameter |
682
- | `setValue` | `(arg: T \| null) => void` | Function to update the query parameter. Null is passed to remove the parameter. |
683
-
684
- ### Built-in Adapters
685
-
686
- | Adapter | Input | Output | Description |
687
- |---------|-------|--------|-------------|
688
- | `adapter.string` | `string[]` | `string` | First value or empty string |
689
- | `adapter.stringArray` | `string[]` | `string[]` | All values as array |
690
- | `adapter.integer` | `string[]` | `number` | First value parsed as integer (default: `0`) |
691
- | `adapter.integerArray` | `string[]` | `number[]` | All values parsed as integers |
692
- | `adapter.float` | `string[]` | `number` | First value parsed as float (default: `0`) |
693
- | `adapter.floatArray` | `string[]` | `number[]` | All values parsed as floats |
694
- | `adapter.boolean` | `string[]` | `boolean` | First value parsed as boolean (`'true'` → `true`) |
695
- | `adapter.booleanArray` | `string[]` | `boolean[]` | All values parsed as booleans |
696
- | `adapter.date` | `string[]` | `Date` | First value parsed as Date from timestamp |
697
- | `adapter.dateArray` | `string[]` | `Date[]` | All values parsed as Dates from timestamps |
698
- | `adapter.zodSchema` | `string[]` | `T` | Validates JSON string against Zod schema |
699
-
700
- ### Using Zod Schemas
701
- `useQueryParam` works seamlessly with Zod for complex validation:
702
-
703
- ```tsx
704
- import { z } from 'zod';
705
- import { useQueryParam, adapter } from 'clear-react-router';
706
-
707
- const filterSchema = z.object({
708
- name: z.string(),
709
- age: z.number().min(0),
710
- active: z.boolean().optional(),
711
- });
712
-
713
- function ProductFilter() {
714
- const [filter, setFilter] = useQueryParam(
715
- 'filter',
716
- adapter.zodSchema(filterSchema),
717
- { name: '', age: 0 }
718
- );
719
-
720
- return (
721
- <div>
722
- <p>Name: {filter.name}</p>
723
- <p>Age: {filter.age}</p>
724
- <button onClick={() => setFilter({ ...filter, age: filter.age + 1 })}>
725
- Increment Age
726
- </button>
727
- </div>
728
- );
729
- }
730
- ```
731
-
732
- ### Custom Adapters
733
- You can write your own adapter for any format:
734
-
735
- ```tsx
736
- // Custom adapter for comma-separated values
737
- const csvAdapter = {
738
- parse: (params: string[]): string[] => {
739
- const value = params[0] || '';
740
- return value ? value.split(',').map(v => v.trim()) : [];
741
- },
742
- serialize: (value: string[]): string[] => value
743
- }
744
-
745
- const TagsFilter() {
746
- const [tags, setTags] = useQueryParam('tags', csvAdapter, []);
747
- // tags: string[]
748
- }
749
- ```
750
-
751
648
  ### `useRouterContext()`
752
649
 
753
650
  Returns the router context object and a function to update it. Useful for accessing or modifying global state (like user authentication, theme, etc.) from anywhere in your app.
package/dist/cell.d.ts CHANGED
@@ -4,6 +4,3 @@ export declare class Cell<T> {
4
4
  get value(): T;
5
5
  set(action: T | ((prev: T) => T)): void;
6
6
  }
7
- export declare const loaderStateRef: Cell<import("./types").LoaderState>;
8
- export declare const prevPathnameRef: Cell<string>;
9
- export declare const timestampMap: Map<string, number>;
@@ -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;
@@ -3,11 +3,12 @@ declare class RouterConfig {
3
3
  routes: RouterProps['routes'];
4
4
  prefetch: RouterProps['prefetch'];
5
5
  isAnimated: RouterProps['isAnimated'];
6
- showFallbackOnAnimation: RouterProps['showFallbackOnAnimation'];
7
6
  hoverPrefetchDelay: number;
8
7
  beforeLoad?: ClientRouteItem['beforeLoad'];
9
8
  afterLoad?: ClientRouteItem['afterLoad'];
10
9
  defaultRetry?: RouterProps['defaultRetry'];
10
+ defaultStaleTime?: RouterProps['defaultStaleTime'];
11
+ defaultPreserveScroll?: RouterProps['defaultPreserveScroll'];
11
12
  configure(config: Partial<RouterConfig>): void;
12
13
  }
13
14
  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.d.ts CHANGED
@@ -9,9 +9,7 @@ export { useInvalidate } from './hooks/useInvalidate';
9
9
  export { useBlocker } from './hooks/useBlocker';
10
10
  export { useAction } from './hooks/useAction';
11
11
  export { useRouterContext } from './hooks/useRouterContext';
12
- export { useQueryParam } from './hooks/useQueryParam';
13
12
  export { useSearchParams } from './hooks/useSearchParams';
14
13
  export { useFormContext } from './hooks/useFormContext';
15
- export { adapter } from './utils/adapter';
16
14
  export { createRouter } from './utils/utils';
17
- export type { RouteItem, BlockerState, Location, Adapter, RouterProps } from './types';
15
+ export type { RouteItem, BlockerState, Location, RouterProps } from './types';
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
@@ -30,15 +30,12 @@ var useGlobalState = ({ subscribe, getState, setState }) => {
30
30
  };
31
31
  //#endregion
32
32
  //#region utils/commitState.ts
33
- var createCommitState = ({ isLoadingState, routeItemDataState, loaderFallbackState, currentLoaderState, prevPathnameRef, loaderStateRef }) => (nextLocation, routeItem) => {
33
+ var createCommitState = ({ routeItemDataState, pendingState }) => (nextLocation, routeItem) => {
34
34
  routeItemDataState.setState({
35
35
  routeItem,
36
36
  location: nextLocation
37
37
  });
38
- currentLoaderState.setState(loaderStateRef.value);
39
- isLoadingState.setState(false);
40
- loaderFallbackState.setState(void 0);
41
- prevPathnameRef.set(nextLocation.pathname);
38
+ pendingState.setState(void 0);
42
39
  const fullPath = nextLocation.search ? `${nextLocation.pathname}${nextLocation.search}` : nextLocation.pathname;
43
40
  if (fullPath === window.location.pathname + window.location.search) return;
44
41
  history.pushState(null, "", fullPath);
@@ -91,11 +88,12 @@ var RouterConfig = class {
91
88
  _defineProperty(this, "routes", []);
92
89
  _defineProperty(this, "prefetch", "hover");
93
90
  _defineProperty(this, "isAnimated", false);
94
- _defineProperty(this, "showFallbackOnAnimation", false);
95
91
  _defineProperty(this, "hoverPrefetchDelay", 150);
96
92
  _defineProperty(this, "beforeLoad", void 0);
97
93
  _defineProperty(this, "afterLoad", void 0);
98
94
  _defineProperty(this, "defaultRetry", void 0);
95
+ _defineProperty(this, "defaultStaleTime", void 0);
96
+ _defineProperty(this, "defaultPreserveScroll", void 0);
99
97
  }
100
98
  configure(config) {
101
99
  Object.assign(this, config);
@@ -104,12 +102,9 @@ var RouterConfig = class {
104
102
  var routerConfig = new RouterConfig();
105
103
  //#endregion
106
104
  //#region utils/commitNavigation.ts
107
- var createCommitNavigation = (navigationExecutor, prevPathnameRef) => (nextLocation, routeItem) => {
108
- const { isAnimated } = routerConfig;
109
- if (!isAnimated || !prevPathnameRef.value) {
110
- navigationExecutor(nextLocation, routeItem);
111
- return;
112
- }
105
+ var createCommitNavigation = (navigationExecutor, routeItemDataState) => (nextLocation, routeItem) => {
106
+ const isFirstLoad = !routeItemDataState.getState().location.pathname;
107
+ if (!routerConfig.isAnimated || isFirstLoad) return navigationExecutor(nextLocation, routeItem);
113
108
  try {
114
109
  document.startViewTransition(() => navigationExecutor(nextLocation, routeItem));
115
110
  } catch {
@@ -120,10 +115,11 @@ var createCommitNavigation = (navigationExecutor, prevPathnameRef) => (nextLocat
120
115
  //#region utils/isCacheItemFresh.ts
121
116
  var createIsCacheItemFresh = (timestampMap) => ({ routeItem, pathname }) => {
122
117
  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;
118
+ const timestamp = timestampMap.get(pathname);
119
+ if (timestamp === void 0) return false;
120
+ const staleTime = routeItem.staleTime ?? routerConfig.defaultStaleTime;
121
+ if (staleTime === void 0) return true;
122
+ return Date.now() - timestamp <= staleTime;
127
123
  };
128
124
  //#endregion
129
125
  //#region ../../node_modules/react/cjs/react-jsx-runtime.production.js
@@ -218,9 +214,10 @@ var findRoute = (pathname, includeAll) => {
218
214
  //#endregion
219
215
  //#region runtime/navigate.ts
220
216
  var navigationSeq = 0;
217
+ var interval = 0;
221
218
  var createNavigate = (routerState, revalidateCache) => {
222
- const { loaderStateRef, scrollMapState, prevPathnameRef, loaderFallbackState, isLoadingState, contextState, timestampMap, pendingPathRef } = routerState;
223
- const commitNavigation = createCommitNavigation(createCommitState(routerState), prevPathnameRef);
219
+ const { loaderStateRef, scrollMapState, pendingState, contextState, timestampMap, routeItemDataState } = routerState;
220
+ const commitNavigation = createCommitNavigation(createCommitState(routerState), routeItemDataState);
224
221
  const isCacheItemFresh = createIsCacheItemFresh(timestampMap);
225
222
  const getContext = () => ({
226
223
  context: contextState.getState(),
@@ -259,29 +256,40 @@ var createNavigate = (routerState, revalidateCache) => {
259
256
  if (routeItem?.beforeLoad) await runBeforeLoad(routeItem?.beforeLoad);
260
257
  };
261
258
  const prepareNavigation = (routeItem, location) => {
262
- const { isAnimated, showFallbackOnAnimation: showFallback } = routerConfig;
263
259
  scrollMapState.setState((prevState) => {
264
260
  const scrollPosition = document.scrollingElement?.scrollTop ?? 0;
265
- if (!scrollPosition || prevState[prevPathnameRef.value] === scrollPosition) return prevState;
261
+ const prevPathname = routeItemDataState.getState().location.pathname;
262
+ if (!scrollPosition || prevState[prevPathname] === scrollPosition) return prevState;
266
263
  return {
267
264
  ...prevState,
268
- [prevPathnameRef.value]: scrollPosition
265
+ [prevPathname]: scrollPosition
269
266
  };
270
267
  });
271
- loaderFallbackState.setState(isCacheItemFresh({
268
+ const pendingShouldExist = routeItem?.loader && !isCacheItemFresh({
272
269
  routeItem,
273
270
  pathname: location.pathname
274
- }) || isAnimated && !showFallback ? void 0 : routeItem?.loaderFallback);
271
+ });
272
+ pendingState.setState(pendingShouldExist ? {
273
+ routeItem,
274
+ location
275
+ } : void 0);
276
+ };
277
+ const afterEachLoad = (routeItem) => {
278
+ if (!routeItem?.pollingInterval) return;
279
+ interval = window.setInterval(() => revalidateCache({
280
+ routeItem,
281
+ pathname: location.pathname
282
+ }), routeItem.pollingInterval);
275
283
  };
276
284
  const loader = async (routeItem, location) => {
277
285
  if (!routeItem?.loader) return;
278
- isLoadingState.setState(true);
279
- pendingPathRef.set(location.pathname);
286
+ window.clearInterval(interval);
280
287
  await revalidateCache({
281
288
  routeItem,
282
- pathname: location.pathname
289
+ pathname: location.pathname,
290
+ search: location.search
283
291
  });
284
- pendingPathRef.set("");
292
+ afterEachLoad(routeItem);
285
293
  };
286
294
  const afterLoad = async (routeItem, params) => {
287
295
  const { afterLoad } = routerConfig;
@@ -399,7 +407,7 @@ var getRetry = (routeItem) => {
399
407
  var sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
400
408
  var createRevalidateCache = (routerState) => {
401
409
  const { loaderStateRef, timestampMap, contextState } = routerState;
402
- const revalidateCache = async ({ routeItem, pathname }, retried = 0) => {
410
+ const revalidateCache = async ({ routeItem, pathname, search = "" }, retried = 0) => {
403
411
  if (!routeItem?.loader) return;
404
412
  const isCacheItemFresh = createIsCacheItemFresh(timestampMap);
405
413
  if (loadingPromises.has(pathname)) return loadingPromises.get(pathname);
@@ -416,18 +424,20 @@ var createRevalidateCache = (routerState) => {
416
424
  const context = contextState.getState();
417
425
  const setContext = contextState.setState;
418
426
  const params = getParamsObject(routeItem, pathname);
427
+ const searchParams = Object.fromEntries(new URLSearchParams(search).entries());
419
428
  const result = await routeItem?.loader({
420
429
  params,
421
430
  context,
422
- setContext
431
+ setContext,
432
+ searchParams
423
433
  });
424
- timestampMap.set(pathname, Date.now());
434
+ timestampMap.set(`${pathname}${search}`, Date.now());
425
435
  loaderStateRef.set((prev) => ({
426
436
  ...prev,
427
437
  data: result,
428
438
  loaderError: null
429
439
  }));
430
- loaderMapRef[pathname] = loaderStateRef.value;
440
+ loaderMapRef[`${pathname}${search}`] = loaderStateRef.value;
431
441
  return {
432
442
  data: result,
433
443
  error: null
@@ -435,7 +445,7 @@ var createRevalidateCache = (routerState) => {
435
445
  } catch (error) {
436
446
  const retry = getRetry(routeItem);
437
447
  if (retry && retry.count > retried) {
438
- loadingPromises.delete(pathname);
448
+ loadingPromises.delete(`${pathname}${search}`);
439
449
  if (retry.delay) await sleep(retry.delay);
440
450
  await revalidateCache({
441
451
  routeItem,
@@ -457,10 +467,10 @@ var createRevalidateCache = (routerState) => {
457
467
  };
458
468
  }
459
469
  } finally {
460
- loadingPromises.delete(pathname);
470
+ loadingPromises.delete(`${pathname}${search}`);
461
471
  }
462
472
  })();
463
- loadingPromises.set(pathname, promise);
473
+ loadingPromises.set(`${pathname}${search}`, promise);
464
474
  return promise;
465
475
  };
466
476
  return revalidateCache;
@@ -479,18 +489,15 @@ var Cell = class {
479
489
  this._value = typeof action === "function" ? action(this._value) : action;
480
490
  }
481
491
  };
482
- new Cell(emptyLoaderState);
483
- new Cell("");
484
492
  //#endregion
485
493
  //#region utils/createRouterInstance.ts
486
494
  var createRouterInstance = () => {
487
495
  const routerState = {
488
- isLoadingState: create(false),
489
- loaderFallbackState: create(void 0),
490
496
  routeItemDataState: create({
491
497
  routeItem: void 0,
492
498
  location: {}
493
499
  }),
500
+ pendingState: create(void 0),
494
501
  currentLoaderState: create(emptyLoaderState),
495
502
  scrollMapState: create({}),
496
503
  contextState: create({}),
@@ -499,13 +506,11 @@ var createRouterInstance = () => {
499
506
  to: ""
500
507
  }),
501
508
  loaderStateRef: new Cell(emptyLoaderState),
502
- prevPathnameRef: new Cell(""),
503
- pendingPathRef: new Cell(""),
504
509
  timestampMap: /* @__PURE__ */ new Map()
505
510
  };
506
511
  const revalidateCache = createRevalidateCache(routerState);
507
- const navigate = createNavigate(routerState, revalidateCache);
508
512
  const invalidate = createInvalidate(routerState, revalidateCache);
513
+ const navigate = createNavigate(routerState, revalidateCache);
509
514
  const prefetch = createPrefetch(revalidateCache);
510
515
  const useGetAction = (actionKey) => {
511
516
  const { routeItem } = routerState.routeItemDataState.getState();
@@ -527,29 +532,17 @@ var createRouterInstance = () => {
527
532
  };
528
533
  };
529
534
  return {
530
- state: {
531
- isLoadingState: routerState.isLoadingState,
532
- loaderFallbackState: routerState.loaderFallbackState,
533
- routeItemDataState: routerState.routeItemDataState,
534
- currentLoaderState: routerState.currentLoaderState,
535
- scrollMapState: routerState.scrollMapState,
536
- contextState: routerState.contextState,
537
- blockedRouteState: routerState.blockedRouteState,
538
- prevPathnameRef: routerState.prevPathnameRef,
539
- pendingPathRef: routerState.pendingPathRef
540
- },
535
+ state: routerState,
541
536
  runtime: {
542
537
  navigate,
543
538
  invalidate,
544
539
  prefetch
545
540
  },
546
541
  hooks: {
547
- useIsLoading: () => useGlobalState(routerState.isLoadingState),
548
542
  useBlockedRoute: () => useGlobalState(routerState.blockedRouteState),
549
- useLoaderFallback: () => useGlobalState(routerState.loaderFallbackState),
550
543
  useRouteItemData: () => useGlobalState(routerState.routeItemDataState),
551
- useCurrentLoaderState: () => useGlobalState(routerState.currentLoaderState),
552
544
  useScrollMap: () => useGlobalState(routerState.scrollMapState),
545
+ usePendingState: () => useGlobalState(routerState.pendingState),
553
546
  useContextState: () => useGlobalState(routerState.contextState),
554
547
  useParams: () => getParamsObject(),
555
548
  useNavigate: () => {
@@ -602,16 +595,16 @@ var router = createRouterInstance();
602
595
  //#endregion
603
596
  //#region hooks/useNavigation.ts
604
597
  var useNavigation = () => {
605
- const { state: { prevPathnameRef, blockedRouteState }, runtime: { navigate } } = router;
598
+ const { state: { routeItemDataState, blockedRouteState }, runtime: { navigate } } = router;
606
599
  useEffect(() => {
607
600
  const handler = async (event) => {
608
601
  const newLocation = parseWindowLocation(event.target.location);
609
- if (prevPathnameRef.value === blockedRouteState.getState().from) {
602
+ if (routeItemDataState.getState().location.pathname === blockedRouteState.getState().from) {
610
603
  blockedRouteState.setState({
611
- from: prevPathnameRef.value,
604
+ from: routeItemDataState.getState().location.pathname,
612
605
  to: newLocation.pathname
613
606
  });
614
- history.pushState(null, "", prevPathnameRef.value);
607
+ history.pushState(null, "", routeItemDataState.getState().location.pathname);
615
608
  } else navigate(newLocation);
616
609
  };
617
610
  window.addEventListener("popstate", handler);
@@ -619,13 +612,11 @@ var useNavigation = () => {
619
612
  }, [
620
613
  blockedRouteState,
621
614
  navigate,
622
- prevPathnameRef.value
615
+ routeItemDataState
623
616
  ]);
624
617
  useEffect(() => {
625
- const currentLocation = parseWindowLocation(window.location);
626
- navigate(currentLocation);
627
- prevPathnameRef.set(currentLocation.pathname);
628
- }, [navigate, prevPathnameRef]);
618
+ navigate(parseWindowLocation(window.location));
619
+ }, [navigate]);
629
620
  };
630
621
  //#endregion
631
622
  //#region hooks/useApplyCustomAnimation.ts
@@ -665,16 +656,10 @@ var useApplyCustomAnimation = (animationDuration) => {
665
656
  }, [animationDuration]);
666
657
  };
667
658
  //#endregion
668
- //#region hooks/useLocation.ts
669
- var useLocation = () => {
670
- const [routeItemData] = router.hooks.useRouteItemData();
671
- return routeItemData.location;
672
- };
673
- //#endregion
674
659
  //#region hooks/usePreserveScroll.ts
675
- var usePreserveScroll = (preserveScroll) => {
660
+ var usePreserveScroll = ({ routeItem, location: { pathname } }) => {
661
+ const preserveScroll = routeItem?.preserveScroll === void 0 ? routerConfig.defaultPreserveScroll : routeItem.preserveScroll;
676
662
  const restoreScroll = router.hooks.useRestoreScroll();
677
- const { pathname } = useLocation();
678
663
  useEffect(() => {
679
664
  if (preserveScroll) restoreScroll();
680
665
  }, [
@@ -686,7 +671,7 @@ var usePreserveScroll = (preserveScroll) => {
686
671
  //#endregion
687
672
  //#region hooks/useSetRouterConfig.ts
688
673
  var useSetRouterConfig = (routerProps) => {
689
- useEffect(() => routerConfig.configure(routerProps), [routerProps]);
674
+ useLayoutEffect(() => routerConfig.configure(routerProps), [routerProps]);
690
675
  };
691
676
  //#endregion
692
677
  //#region hooks/useSetInitialContext.ts
@@ -708,31 +693,32 @@ var renderElement = (Component) => {
708
693
  //#endregion
709
694
  //#region components/Router.tsx
710
695
  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 }) => {
712
- const { useIsLoading, useLoaderFallback, useRouteItemData, useCurrentLoaderState } = router.hooks;
713
- const [isLoading] = useIsLoading();
714
- const [currentLoaderFallback] = useLoaderFallback();
696
+ 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 }) => {
697
+ const { useRouteItemData, usePendingState } = router.hooks;
715
698
  const [routeItemData] = useRouteItemData();
716
- const [loaderState] = useCurrentLoaderState();
699
+ const [pendingState] = usePendingState();
700
+ const loaderState = router.state.loaderStateRef.value;
701
+ const isLoading = Boolean(pendingState);
717
702
  useNavigation();
718
703
  useSetRouterConfig({
719
704
  routes,
720
705
  isAnimated,
721
706
  prefetch,
722
707
  hoverPrefetchDelay,
723
- showFallbackOnAnimation,
724
708
  beforeLoad,
725
709
  afterLoad,
726
- defaultRetry
710
+ defaultRetry,
711
+ defaultStaleTime,
712
+ defaultPreserveScroll
727
713
  });
728
714
  useApplyCustomAnimation(animationDuration);
729
715
  useSetInitialContext(initialContext);
730
- usePreserveScroll(preserveScroll);
716
+ usePreserveScroll(routeItemData);
717
+ const { routeItem, location } = routeItemData;
731
718
  const showErrorElement = !isLoading && Boolean(loaderState.loaderError || loaderState.beforeLoadError);
732
719
  const showSpinner = spinner && isAnimated && isLoading;
733
720
  const loadingContent = !showErrorElement && isLoading;
734
- const { routeItem, location } = routeItemData;
735
- if ((showFallbackOnAnimation || !isAnimated) && loadingContent) return renderElement(currentLoaderFallback || defaultLoaderFallback);
721
+ if ((showFallbackOnAnimation || !isAnimated) && loadingContent) return renderElement(pendingState?.routeItem?.loaderFallback || defaultLoaderFallback);
736
722
  if (!showFallbackOnAnimation && isAnimated && loadingContent) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
737
723
  if (!routeItem) return null;
738
724
  if (showErrorElement) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [renderElement(routeItem.errorElement || defaultErrorElement), showSpinner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})] });
@@ -744,14 +730,20 @@ var Router = ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated = f
744
730
  //#endregion
745
731
  //#region hooks/useIsRoutePending.ts
746
732
  var useIsRoutePending = (routePath) => {
747
- const { hooks: { useIsLoading }, state: { pendingPathRef } } = router;
748
- const [isPending] = useIsLoading();
749
- return isPending && pendingPathRef.value === routePath;
733
+ const { usePendingState } = router.hooks;
734
+ const [pendingState] = usePendingState();
735
+ return pendingState?.location.pathname === routePath;
750
736
  };
751
737
  //#endregion
752
738
  //#region hooks/useNavigate.ts
753
739
  var useNavigate = router.hooks.useNavigate;
754
740
  //#endregion
741
+ //#region hooks/useLocation.ts
742
+ var useLocation = () => {
743
+ const [routeItemData] = router.hooks.useRouteItemData();
744
+ return routeItemData.location;
745
+ };
746
+ //#endregion
755
747
  //#region components/Link.tsx
756
748
  var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, className, style, onClick, activeClassName = "active-link", pendingClassName = "pending-link" }) => {
757
749
  const isPending = useIsRoutePending(to);
@@ -867,10 +859,7 @@ var Form = ({ children, action, onSuccess, onError, autoReset = true }) => {
867
859
  var useParams = router.hooks.useParams;
868
860
  //#endregion
869
861
  //#region hooks/useLoaderState.ts
870
- var useLoaderState = () => {
871
- const [loaderState] = router.hooks.useCurrentLoaderState();
872
- return loaderState;
873
- };
862
+ var useLoaderState = () => router.state.loaderStateRef.value;
874
863
  //#endregion
875
864
  //#region hooks/useInvalidate.ts
876
865
  var useInvalidate = () => router.runtime.invalidate;
@@ -960,19 +949,9 @@ var useSearch = () => {
960
949
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
961
950
  };
962
951
  //#endregion
963
- //#region hooks/useLatest.ts
964
- var useLatest = (value) => {
965
- const ref = useRef(value);
966
- useEffect(() => {
967
- ref.current = value;
968
- }, [value]);
969
- return ref;
970
- };
971
- //#endregion
972
952
  //#region hooks/useSearchParams.ts
973
953
  var useSearchParams = () => {
974
954
  const search = useSearch();
975
- const searchRef = useLatest(search);
976
955
  const searchString = search ? search.replace("?", "") : window.location.pathname.split("?")?.[1] ?? "";
977
956
  const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
978
957
  const getSearchParams = useCallback((param) => {
@@ -989,105 +968,18 @@ var useSearchParams = () => {
989
968
  searchParams,
990
969
  getSearchParams,
991
970
  setSearchParams: useCallback((param, value) => {
992
- const currentParams = new URLSearchParams(searchRef.current);
971
+ const currentParams = new URLSearchParams(search);
993
972
  if (typeof param === "string" && value !== void 0) {
994
973
  currentParams.delete(param);
995
974
  (Array.isArray(value) ? value : [value]).forEach((v) => currentParams.append(param, v));
996
975
  navigateWithSearchParams(currentParams);
997
976
  } else if (typeof param === "function") navigateWithSearchParams(param(currentParams));
998
977
  else throw new Error("useSearchParams first argument must be either function or string");
999
- }, [navigateWithSearchParams, searchRef])
978
+ }, [navigateWithSearchParams, search])
1000
979
  };
1001
980
  };
1002
981
  //#endregion
1003
- //#region hooks/useQueryParam.ts
1004
- function useQueryParam(field, adapter, defaultValue) {
1005
- const { searchParams, setSearchParams } = useSearchParams();
1006
- return [useMemo(() => {
1007
- const params = searchParams.getAll(field);
1008
- const result = adapter.parse(params);
1009
- const isValid = !(result instanceof Date) || result instanceof Date && !isNaN(result.getTime());
1010
- if (result !== void 0 && result !== null && result !== "" && isValid) return result;
1011
- if (defaultValue !== void 0) return defaultValue;
1012
- return result;
1013
- }, [
1014
- field,
1015
- adapter,
1016
- searchParams,
1017
- defaultValue
1018
- ]), useCallback((value) => {
1019
- if (!value) return setSearchParams(field, []);
1020
- setSearchParams(field, (adapter.serialize || String)(value));
1021
- }, [
1022
- field,
1023
- adapter.serialize,
1024
- setSearchParams
1025
- ])];
1026
- }
1027
- //#endregion
1028
982
  //#region hooks/useFormContext.ts
1029
983
  var useFormContext = () => useContext(FormContext);
1030
984
  //#endregion
1031
- //#region utils/adapter.ts
1032
- var adapter = {
1033
- string: { parse: (params) => params[0] || "" },
1034
- stringArray: {
1035
- parse: (params) => params,
1036
- serialize: (value) => value
1037
- },
1038
- integer: { parse: (params) => {
1039
- const result = parseInt(params[0] || "");
1040
- return isNaN(result) ? 0 : result;
1041
- } },
1042
- integerArray: {
1043
- parse: (params) => params.map((el) => {
1044
- const result = parseInt(el);
1045
- return isNaN(result) ? 0 : result;
1046
- }),
1047
- serialize: (value) => value.map(String)
1048
- },
1049
- float: { parse: (params) => {
1050
- const result = parseFloat(params[0] || "");
1051
- return isNaN(result) ? 0 : result;
1052
- } },
1053
- floatArray: {
1054
- parse: (params) => params.map((el) => {
1055
- const result = parseFloat(el);
1056
- return isNaN(result) ? 0 : result;
1057
- }),
1058
- serialize: (value) => value.map(String)
1059
- },
1060
- boolean: {
1061
- parse: (params) => params[0]?.toLowerCase() === "true",
1062
- serialize: String
1063
- },
1064
- booleanArray: {
1065
- parse: (params) => params.map((el) => el.toLowerCase() === "true"),
1066
- serialize: (value) => value.map(String)
1067
- },
1068
- date: {
1069
- parse: (params) => new Date(Number(params[0])),
1070
- serialize: (arg) => String(arg.getTime())
1071
- },
1072
- dateArray: {
1073
- parse: (params) => params.map((param) => new Date(Number(param))),
1074
- serialize: (args) => args.map((arg) => String(arg.getTime()))
1075
- },
1076
- zodSchema: (schema) => ({
1077
- parse: (params) => {
1078
- let parsed;
1079
- try {
1080
- parsed = params[0] ? JSON.parse(params[0]) : void 0;
1081
- } catch {
1082
- throw new Error("Invalid JSON");
1083
- }
1084
- if (parsed === void 0) return void 0;
1085
- const result = schema.safeParse(parsed);
1086
- if (!result.success) throw new Error("Invalid schema");
1087
- return result.data;
1088
- },
1089
- serialize: JSON.stringify
1090
- })
1091
- };
1092
- //#endregion
1093
- export { Form, Link, Router, adapter, createRouter, useAction, useBlocker, useFormContext, useInvalidate, useLoaderState, useLocation, useNavigate, useParams, useQueryParam, useRouterContext, useSearchParams };
985
+ export { Form, Link, Router, createRouter, useAction, useBlocker, useFormContext, useInvalidate, useLoaderState, useLocation, useNavigate, useParams, useRouterContext, useSearchParams };
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,16 +58,13 @@ 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;
61
65
  loaderError: Error | null;
62
66
  beforeLoadError: Error | null;
63
67
  };
64
- export type Adapter<T> = {
65
- parse: (params: string[]) => T;
66
- serialize?: (params: T) => string | string[];
67
- };
68
68
  export type RouteItemData = {
69
69
  location: Location;
70
70
  routeItem: RouteItem | undefined;
@@ -79,8 +79,9 @@ export type RouterProps = {
79
79
  isAnimated?: boolean;
80
80
  animationDuration?: number;
81
81
  spinner?: boolean;
82
- preserveScroll?: boolean;
82
+ defaultPreserveScroll?: boolean;
83
83
  defaultRetry?: Retry;
84
+ defaultStaleTime?: number;
84
85
  defaultLoaderFallback?: RenderElement;
85
86
  defaultErrorElement?: RenderElement;
86
87
  showFallbackOnAnimation?: boolean;
@@ -94,9 +95,8 @@ export type RouterProps = {
94
95
  context?: Record<string, unknown>;
95
96
  };
96
97
  export type RouterState = {
97
- isLoadingState: Store<boolean>;
98
- loaderFallbackState: Store<RouteItem['loaderFallback']>;
99
98
  routeItemDataState: Store<RouteItemData>;
99
+ pendingState: Store<RouteItemData | undefined>;
100
100
  currentLoaderState: Store<LoaderState>;
101
101
  scrollMapState: Store<Record<string, number>>;
102
102
  contextState: Store<Record<string, unknown>>;
@@ -105,27 +105,23 @@ export type RouterState = {
105
105
  to: string;
106
106
  }>;
107
107
  loaderStateRef: Cell<LoaderState>;
108
- prevPathnameRef: Cell<string>;
109
- pendingPathRef: Cell<string>;
110
108
  timestampMap: Map<string, number>;
111
109
  };
112
110
  export type RouterType = {
113
- state: Omit<RouterState, 'loaderStateRef' | 'timestampMap'>;
111
+ state: Omit<RouterState, 'timestampMap'>;
114
112
  runtime: {
115
113
  navigate(arg: Location): Promise<void>;
116
114
  invalidate(pathList?: string | string[], options?: InvalidateOptions): Promise<InvalidateResult[]>;
117
115
  prefetch(pathname: string): Promise<void>;
118
116
  };
119
117
  hooks: {
120
- useIsLoading: () => ReturnType<typeof useGlobalState<boolean>>;
121
118
  useBlockedRoute: () => ReturnType<typeof useGlobalState<{
122
119
  from: string;
123
120
  to: string;
124
121
  }>>;
125
- useLoaderFallback: () => ReturnType<typeof useGlobalState<RenderElement | undefined>>;
126
122
  useRouteItemData: () => ReturnType<typeof useGlobalState<RouteItemData>>;
127
- useCurrentLoaderState: () => ReturnType<typeof useGlobalState<LoaderState>>;
128
123
  useScrollMap: () => ReturnType<typeof useGlobalState<Record<string, number>>>;
124
+ usePendingState: () => ReturnType<typeof useGlobalState<RouteItemData | undefined>>;
129
125
  useContextState: () => ReturnType<typeof useGlobalState<Record<string, unknown>>>;
130
126
  useParams: <T>() => T;
131
127
  useNavigate: () => (arg: Location | string | -1) => Promise<void>;
@@ -1,3 +1,3 @@
1
- import { Cell } from '../cell';
2
- import { Location, RouteItem } from '../types';
3
- export declare const createCommitNavigation: (navigationExecutor: (arg: Location, routeItem: RouteItem | undefined) => void, prevPathnameRef: Cell<string>) => (nextLocation: Location, routeItem: RouteItem | undefined) => void;
1
+ import { Store } from '../create';
2
+ import { Location, RouteItem, RouteItemData } from '../types';
3
+ export declare const createCommitNavigation: (navigationExecutor: (arg: Location, routeItem: RouteItem | undefined) => void, routeItemDataState: Store<RouteItemData>) => (nextLocation: Location, routeItem: RouteItem | undefined) => void;
@@ -1,2 +1,2 @@
1
1
  import { Location, RouteItem, RouterState } from '../types';
2
- export declare const createCommitState: ({ isLoadingState, routeItemDataState, loaderFallbackState, currentLoaderState, prevPathnameRef, loaderStateRef, }: RouterState) => (nextLocation: Location, routeItem: RouteItem | undefined) => void;
2
+ export declare const createCommitState: ({ routeItemDataState, pendingState }: RouterState) => (nextLocation: Location, routeItem: RouteItem | undefined) => void;
@@ -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.1",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {
@@ -1 +0,0 @@
1
- export declare const useLatest: <T>(value: T) => import("react").RefObject<T>;
@@ -1,2 +0,0 @@
1
- import { Adapter } from '../types';
2
- export declare function useQueryParam<T>(field: string, adapter: Adapter<T>, defaultValue?: T): [T, (arg: T | null) => void];
@@ -1,51 +0,0 @@
1
- import { Adapter } from '../types';
2
- type ZodInterface<T> = {
3
- safeParse(input: unknown): {
4
- success: true;
5
- data: T;
6
- } | {
7
- success: false;
8
- error: unknown;
9
- };
10
- };
11
- export declare const adapter: {
12
- string: {
13
- parse: (params: string[]) => string;
14
- };
15
- stringArray: {
16
- parse: (params: string[]) => string[];
17
- serialize: (value: string[]) => string[];
18
- };
19
- integer: {
20
- parse: (params: string[]) => number;
21
- };
22
- integerArray: {
23
- parse: (params: string[]) => number[];
24
- serialize: (value: number[]) => string[];
25
- };
26
- float: {
27
- parse: (params: string[]) => number;
28
- };
29
- floatArray: {
30
- parse: (params: string[]) => number[];
31
- serialize: (value: number[]) => string[];
32
- };
33
- boolean: {
34
- parse: (params: string[]) => boolean;
35
- serialize: StringConstructor;
36
- };
37
- booleanArray: {
38
- parse: (params: string[]) => boolean[];
39
- serialize: (value: boolean[]) => string[];
40
- };
41
- date: {
42
- parse: (params: string[]) => Date;
43
- serialize: (arg: Date) => string;
44
- };
45
- dateArray: {
46
- parse: (params: string[]) => Date[];
47
- serialize: (args: Date[]) => string[];
48
- };
49
- zodSchema: <T>(schema: ZodInterface<T>) => Adapter<T>;
50
- };
51
- export {};