clear-react-router 1.5.7 → 1.6.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
@@ -72,6 +72,8 @@ Renders the current route's component. Must be placed inside `<RouterProvider>`.
72
72
  | `spinner` | `boolean \| undefined` | `true` | Show a small spinner in the corner while loading data (only when `isAnimated` is enabled) |
73
73
  | `preserveScroll` | `boolean \| undefined` | `true` | Save and restore scroll position when navigating between pages |
74
74
  | `showFallbackIfAnimated` | `boolean \| undefined` | `false` | Show `loaderFallback` even when `isAnimated` is `true` (instead of spinner) |
75
+ | `prefetch` | `'hover' \| 'render' \| 'viewport' \| 'none'` | `'hover'` | Default prefetch strategy for all `<Link>` components |
76
+ | `hoverPrefetchDelay` | `number` | `150` | Delay in milliseconds before prefetching on hover (only for `'hover'` strategy) |
75
77
 
76
78
  ```
77
79
  <RouterProvider routeList={routes} isAnimated>
@@ -89,20 +91,40 @@ Component for client-side navigation with prefetch support.
89
91
  | Prop | Type | Default |
90
92
  |------|------|---------|
91
93
  | `to` | `string` | required |
92
- | `prefetch` | `boolean` optional | `true` |
93
- | `prefetchDelay` | `number` optional | `150` | Delay in ms before prefetch starts (prevents unnecessary requests on quick mouse passes) |
94
- | `children` | `ReactElement` | required |
94
+ | `prefetch` | `'hover' \| 'render' \| 'viewport' \| 'none'` | `Router` config | Override the global prefetch strategy |
95
+ | `hoverPrefetchDelay` | `number` | `Router` config | Override the global hover delay |
96
+ | `children` | `ReactElement` | required | Content to render inside the link |
97
+
98
+ ### Prefetch Strategies
99
+
100
+ | Strategy | Behavior |
101
+ |----------|----------|
102
+ | `'hover'` | Prefetches when the user hovers over the link (with configurable delay) |
103
+ | `'render'` | Prefetches immediately when the link is rendered |
104
+ | `'viewport'` | Prefetches when the link enters the viewport (using Intersection Observer) |
105
+ | `'none'` | No prefetching |
106
+
107
+ **Example:**
95
108
 
96
109
  ```
97
- // Default prefetch after 150ms hover
98
- <Link to="/dashboard">Dashboard</Link>
110
+ import { RouterProvider, Router, Link } from 'clear-react-router';
111
+
112
+ // Global prefetch: hover with 100ms delay
113
+ <RouterProvider routeList={routes} prefetch="hover" hoverPrefetchDelay={100}>
114
+ <Router />
115
+ </RouterProvider>
99
116
 
100
- // Custom prefetch delay (300ms)
101
- <Link to="/heavy-page" prefetchDelay={300}>Heavy Page</Link>
117
+ // Override for a specific link
118
+ <Link to="/heavy-page" prefetch="viewport">
119
+ Heavy Page
120
+ </Link>
102
121
 
103
- // Disable prefetch
104
- <Link to="/about" prefetch={false}>About</Link>
122
+ // Disable prefetch for a specific link
123
+ <Link to="/admin" prefetch="none">
124
+ Admin Panel
125
+ </Link>
105
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.
106
128
 
107
129
  ### `redirect`
108
130
 
@@ -1,12 +1,13 @@
1
1
  import { type ReactElement, type MouseEvent, type CSSProperties } from 'react';
2
+ import { RouterProps } from '../types/global';
2
3
  type LinkProps = {
3
4
  to: string;
4
5
  children: ReactElement<{
5
6
  onClick: (e: MouseEvent) => void;
6
7
  style: CSSProperties;
7
8
  }>;
8
- prefetch?: boolean;
9
- prefetchDelay?: number;
9
+ prefetch?: RouterProps['prefetch'];
10
+ hoverPrefetchDelay?: number;
10
11
  };
11
- export declare const Link: ({ children, to, prefetch, prefetchDelay }: LinkProps) => import("react/jsx-runtime").JSX.Element;
12
+ export declare const Link: ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }: LinkProps) => import("react/jsx-runtime").JSX.Element;
12
13
  export {};
@@ -1,9 +1,2 @@
1
- type RouterProps = {
2
- isAnimated?: boolean;
3
- animationDuration?: number;
4
- spinner?: boolean;
5
- preserveScroll?: boolean;
6
- showFallbackIfAnimated?: boolean;
7
- };
8
- export declare const Router: ({ isAnimated, animationDuration, spinner, preserveScroll, showFallbackIfAnimated, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
9
- export {};
1
+ import { RouterProps } from '../types/global';
2
+ export declare const Router: ({ isAnimated, animationDuration, spinner, preserveScroll, showFallbackIfAnimated, prefetch, hoverPrefetchDelay, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -1,6 +1,9 @@
1
+ import { RouterProps } from '../types/global';
1
2
  declare class RouterConfig {
2
3
  isAnimated: boolean;
3
4
  showFallbackIfAnimated: boolean;
5
+ prefetch: RouterProps['prefetch'];
6
+ hoverPrefetchDelay: number;
4
7
  configure(config: Partial<RouterConfig>): void;
5
8
  }
6
9
  export declare const routerConfig: RouterConfig;
@@ -0,0 +1,3 @@
1
+ import type { LoaderState } from './types/global.ts';
2
+ export declare const emptyLoaderState: LoaderState;
3
+ export declare const STANDARD_PREFETCH_DELAY = 150;
@@ -1,12 +1,12 @@
1
- import { BlockerState, Location, RouteItem, RouteItemData, UpdateBlockedRouteProps } from '../types/global';
1
+ import { BlockerState, LoaderState, Location, RouteItem, RouteItemData, UpdateBlockedRouteProps } from '../types/global';
2
2
  export type NavigationContextValue = {
3
3
  blockerState: BlockerState;
4
4
  routeItemData: RouteItemData;
5
5
  currentLoaderFallback: RouteItem['loaderFallback'];
6
6
  isLoading: boolean;
7
+ loaderState: LoaderState;
7
8
  };
8
9
  export type ActionsContextValue = {
9
- setSearch(arg: string): void;
10
10
  updateLocation(route: Location): Promise<void>;
11
11
  updateBlockedRoute(arg: UpdateBlockedRouteProps): void;
12
12
  prefetchLoader(arg: string): Promise<void>;
@@ -1,23 +1,24 @@
1
- import { type Dispatch, type SetStateAction } from 'react';
2
- import { BlockerState, Location, RevalidateCacheArgs, RouteItem, RouteItemData, UpdateBlockedRouteProps } from '../types/global';
1
+ import { type Dispatch, RefObject, type SetStateAction } from 'react';
2
+ import { BlockerState, LoaderState, Location, RevalidateCacheArgs, RouteItem, RouteItemData, UpdateBlockedRouteProps } from '../types/global';
3
3
  type UseHandleNavigation = {
4
4
  routeList: RouteItem[];
5
5
  context: Record<string, unknown>;
6
- revalidateCache(arg: RevalidateCacheArgs): Promise<void>;
6
+ revalidateCache(arg: RevalidateCacheArgs): Promise<unknown>;
7
7
  setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
8
8
  isCacheItemFresh(arg: {
9
9
  routeItem?: RouteItem;
10
10
  pathname: string;
11
11
  }): boolean;
12
- setIsLoading(arg: boolean): void;
12
+ loaderStateRef: RefObject<LoaderState>;
13
13
  };
14
- export declare const useHandleNavigation: ({ routeList, context, revalidateCache, setContext, isCacheItemFresh, setIsLoading, }: UseHandleNavigation) => {
14
+ export declare const useHandleNavigation: ({ routeList, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef, }: UseHandleNavigation) => {
15
15
  blockerState: BlockerState;
16
16
  updateLocation: (nextLocation: Location) => Promise<void>;
17
17
  updateBlockedRoute: ({ type, payload }: UpdateBlockedRouteProps) => void;
18
18
  routeItemData: RouteItemData;
19
- setSearch: (search: string) => void;
20
19
  restoreScroll: () => void;
21
20
  currentLoaderFallback: import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | (() => import("react").ReactElement) | undefined;
21
+ isLoading: boolean;
22
+ loaderState: LoaderState;
22
23
  };
23
24
  export {};
@@ -1,5 +1,5 @@
1
1
  import { type Dispatch, type SetStateAction } from 'react';
2
- import type { RevalidateCacheArgs, RouteItem } from '../types/global';
2
+ import type { LoaderState, RevalidateCacheArgs, RouteItem } from '../types/global';
3
3
  type UseLoaderParams = {
4
4
  routeList: RouteItem[];
5
5
  context: Record<string, unknown>;
@@ -7,12 +7,11 @@ type UseLoaderParams = {
7
7
  };
8
8
  export declare const useLoader: ({ routeList, context, setContext }: UseLoaderParams) => {
9
9
  prefetchLoader: (pathname: string) => Promise<void>;
10
- revalidateCache: ({ routeItem, loaderState, pathname }: RevalidateCacheArgs) => Promise<void>;
10
+ revalidateCache: ({ routeItem, pathname }: RevalidateCacheArgs) => Promise<unknown>;
11
11
  isCacheItemFresh: ({ routeItem, pathname }: {
12
12
  routeItem?: RouteItem;
13
13
  pathname: string;
14
14
  }) => boolean;
15
- isLoading: boolean;
16
- setIsLoading: Dispatch<SetStateAction<boolean>>;
15
+ loaderStateRef: import("react").RefObject<LoaderState>;
17
16
  };
18
17
  export {};
@@ -0,0 +1 @@
1
+ export declare const useSearch: () => string;
@@ -0,0 +1,2 @@
1
+ import { RouterProps } from '../types/global';
2
+ export declare const useSetRouterConfig: (routerProps: RouterProps) => void;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Suspense, createContext, lazy, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
1
+ import { Suspense, createContext, lazy, useCallback, useContext, useEffect, 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
@@ -45,10 +45,9 @@ var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((expor
45
45
  var import_jsx_runtime = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
46
46
  module.exports = require_react_jsx_runtime_production();
47
47
  })))();
48
- var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocation, setSearch, prefetchLoader, blockerState, routeItemData, restoreScroll, currentLoaderFallback, isLoading }) => {
48
+ var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocation, prefetchLoader, blockerState, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState }) => {
49
49
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionsContext.Provider, {
50
50
  value: {
51
- setSearch,
52
51
  updateLocation,
53
52
  updateBlockedRoute,
54
53
  prefetchLoader,
@@ -62,7 +61,8 @@ var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocat
62
61
  blockerState,
63
62
  routeItemData,
64
63
  currentLoaderFallback,
65
- isLoading
64
+ isLoading,
65
+ loaderState
66
66
  },
67
67
  children
68
68
  })
@@ -79,6 +79,9 @@ var useLatest = (value) => {
79
79
  return ref;
80
80
  };
81
81
  //#endregion
82
+ //#region constants.ts
83
+ var emptyLoaderState = {};
84
+ //#endregion
82
85
  //#region \0@oxc-project+runtime@0.133.0/helpers/esm/typeof.js
83
86
  function _typeof(o) {
84
87
  "@babel/helpers - typeof";
@@ -122,6 +125,8 @@ var RouterConfig = class {
122
125
  constructor() {
123
126
  _defineProperty(this, "isAnimated", false);
124
127
  _defineProperty(this, "showFallbackIfAnimated", false);
128
+ _defineProperty(this, "prefetch", "hover");
129
+ _defineProperty(this, "hoverPrefetchDelay", 150);
125
130
  }
126
131
  configure(config) {
127
132
  Object.assign(this, config);
@@ -182,30 +187,23 @@ var comparePaths = (el, pathname) => {
182
187
  //#endregion
183
188
  //#region hooks/useHandleNavigation.ts
184
189
  var ALL_LOCATIONS = "*";
185
- var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, isCacheItemFresh, setIsLoading }) => {
190
+ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef }) => {
186
191
  const { isAnimated, showFallbackIfAnimated: showFallback } = routerConfig;
192
+ const [isLoading, setIsLoading] = useState(false);
187
193
  const [blockedRoute, setBlockedRoute] = useState({
188
194
  from: "",
189
195
  to: ""
190
196
  });
191
197
  const [routeItemData, setRouteItemData] = useState({
192
- location: {},
193
198
  routeItem: void 0,
194
- loaderState: {}
199
+ location: {}
195
200
  });
196
201
  const [scrollMap, setScrollMap] = useState({});
197
202
  const [currentLoaderFallback, setCurrentLoaderFallback] = useState();
198
- const loaderState = useRef({});
203
+ const [loaderState, setLoaderState] = useState(emptyLoaderState);
199
204
  const prevPathname = useRef("");
200
205
  const navigationSeq = useRef(0);
201
206
  const scrollMapRef = useLatest(scrollMap);
202
- const setSearch = useCallback((search) => setRouteItemData((prevState) => ({
203
- ...prevState,
204
- location: {
205
- ...prevState.location,
206
- search
207
- }
208
- })), []);
209
207
  const restoreScroll = useCallback(() => {
210
208
  if (!prevPathname.current || !scrollMapRef.current[prevPathname.current]) return;
211
209
  requestAnimationFrame(() => {
@@ -217,17 +215,17 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
217
215
  }, [scrollMapRef]);
218
216
  const navigation = useCallback((nextLocation, routeItem) => {
219
217
  setRouteItemData({
220
- location: nextLocation,
221
218
  routeItem,
222
- loaderState: loaderState.current
219
+ location: nextLocation
223
220
  });
221
+ setLoaderState(loaderStateRef.current);
224
222
  setIsLoading(false);
225
223
  setCurrentLoaderFallback(void 0);
226
224
  prevPathname.current = nextLocation.pathname;
227
225
  const fullPath = nextLocation.search ? `${nextLocation.pathname}${nextLocation.search}` : nextLocation.pathname;
228
226
  if (fullPath === window.location.pathname + window.location.search) return;
229
227
  history.pushState(null, "", fullPath);
230
- }, []);
228
+ }, [loaderStateRef]);
231
229
  const transitionedNavigation = useCallback((nextLocation, routeItem) => {
232
230
  if (!isAnimated) {
233
231
  navigation(nextLocation, routeItem);
@@ -242,7 +240,7 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
242
240
  const navigationHandler = useCallback(async (nextLocation) => {
243
241
  navigationSeq.current = navigationSeq.current + 1;
244
242
  const seq = navigationSeq.current;
245
- loaderState.current = {};
243
+ loaderStateRef.current = emptyLoaderState;
246
244
  const nextItem = routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, nextLocation.pathname));
247
245
  const params = getParamsObject({
248
246
  params: nextItem?.params,
@@ -256,13 +254,13 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
256
254
  params,
257
255
  setContext
258
256
  });
259
- loaderState.current = {
260
- ...loaderState.current,
257
+ loaderStateRef.current = {
258
+ ...loaderStateRef.current,
261
259
  beforeLoadError: null
262
260
  };
263
261
  } catch (error) {
264
- loaderState.current = {
265
- ...loaderState.current,
262
+ loaderStateRef.current = {
263
+ ...loaderStateRef.current,
266
264
  beforeLoadError: error
267
265
  };
268
266
  return transitionedNavigation(nextLocation, nextItem);
@@ -280,11 +278,13 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
280
278
  routeItem: nextItem,
281
279
  pathname: nextLocation.pathname
282
280
  }) || isAnimated && !showFallback ? void 0 : nextItem?.loaderFallback);
283
- await revalidateCache({
284
- routeItem: nextItem,
285
- loaderState,
286
- pathname: nextLocation.pathname
287
- });
281
+ if (nextItem?.loader) {
282
+ setIsLoading(true);
283
+ await revalidateCache({
284
+ routeItem: nextItem,
285
+ pathname: nextLocation.pathname
286
+ });
287
+ }
288
288
  if (seq !== navigationSeq.current) return;
289
289
  transitionedNavigation(nextLocation, nextItem);
290
290
  if (nextItem?.afterLoad) await nextItem.afterLoad({
@@ -300,7 +300,8 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
300
300
  setContext,
301
301
  isCacheItemFresh,
302
302
  isAnimated,
303
- showFallback
303
+ showFallback,
304
+ loaderStateRef
304
305
  ]);
305
306
  const setNextLocationRef = useLatest(navigationHandler);
306
307
  const updateBlockedRoute = useCallback(({ type, payload = "" }) => setBlockedRoute((prevState) => {
@@ -355,69 +356,74 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
355
356
  updateLocation,
356
357
  updateBlockedRoute,
357
358
  routeItemData,
358
- setSearch,
359
359
  restoreScroll,
360
- currentLoaderFallback
360
+ currentLoaderFallback,
361
+ isLoading,
362
+ loaderState
361
363
  };
362
364
  };
363
365
  //#endregion
364
366
  //#region hooks/useLoader.ts
365
367
  var useLoader = ({ routeList, context, setContext }) => {
366
- const [isLoading, setIsLoading] = useState(false);
367
- const cacheTimestampsRef = useRef({});
368
- const loaderCacheRef = useRef({});
368
+ const timestampMapRef = useRef({});
369
+ const loaderMapRef = useRef({});
370
+ const loaderStateRef = useRef(emptyLoaderState);
371
+ const loadingPromises = useRef(/* @__PURE__ */ new Map());
369
372
  const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
370
373
  if (!routeItem) return true;
371
- const currentCacheTimestamp = cacheTimestampsRef.current[pathname];
374
+ const currentCacheTimestamp = timestampMapRef.current[pathname];
372
375
  if (!currentCacheTimestamp) return false;
373
376
  if (!routeItem.staleTime) return true;
374
377
  return Date.now() - currentCacheTimestamp < routeItem.staleTime;
375
378
  }, []);
376
- const revalidateCache = useCallback(async ({ routeItem, loaderState, pathname }) => {
377
- if (!routeItem?.loader) {
378
- if (!loaderState) return;
379
- if (!routeItem?.beforeLoad) loaderState.current = {};
380
- return;
381
- }
379
+ const revalidateCache = useCallback(async ({ routeItem, pathname }) => {
380
+ if (!routeItem?.loader) return;
381
+ if (loadingPromises.current.has(pathname)) return loadingPromises.current.get(pathname);
382
382
  if (isCacheItemFresh({
383
383
  routeItem,
384
384
  pathname
385
385
  })) {
386
- if (loaderState) loaderState.current = loaderCacheRef.current[pathname];
386
+ loaderStateRef.current = loaderMapRef.current[pathname];
387
387
  return;
388
388
  }
389
- if (loaderState) setIsLoading(true);
390
- try {
391
- const params = getParamsObject({
392
- params: routeItem.params,
393
- pathname
394
- });
395
- const result = await routeItem?.loader({
396
- params,
397
- context,
398
- setContext
399
- });
400
- cacheTimestampsRef.current = {
401
- ...cacheTimestampsRef.current,
402
- [pathname]: Date.now()
403
- };
404
- loaderCacheRef.current[pathname] = {
405
- data: result,
406
- loaderError: null,
407
- beforeLoadError: null
408
- };
409
- if (loaderState) loaderState.current = {
410
- ...loaderState?.current,
411
- data: result,
412
- loaderError: null
413
- };
414
- } catch (error) {
415
- if (loaderState) loaderState.current = {
416
- ...loaderState?.current,
417
- data: null,
418
- loaderError: error
419
- };
420
- }
389
+ const promise = (async () => {
390
+ if (!routeItem?.loader) return;
391
+ try {
392
+ const params = getParamsObject({
393
+ params: routeItem.params,
394
+ pathname
395
+ });
396
+ const result = await routeItem?.loader({
397
+ params,
398
+ context,
399
+ setContext
400
+ });
401
+ timestampMapRef.current = {
402
+ ...timestampMapRef.current,
403
+ [pathname]: Date.now()
404
+ };
405
+ loaderMapRef.current[pathname] = {
406
+ data: result,
407
+ loaderError: null,
408
+ beforeLoadError: null
409
+ };
410
+ loaderStateRef.current = {
411
+ ...loaderStateRef?.current,
412
+ data: result,
413
+ loaderError: null
414
+ };
415
+ } catch (error) {
416
+ loaderStateRef.current = {
417
+ ...loaderStateRef?.current,
418
+ data: null,
419
+ loaderError: error
420
+ };
421
+ } finally {
422
+ loadingPromises.current.delete(pathname);
423
+ }
424
+ })();
425
+ loadingPromises.current.set(pathname, promise);
426
+ return promise;
421
427
  }, [
422
428
  context,
423
429
  isCacheItemFresh,
@@ -433,30 +439,28 @@ var useLoader = ({ routeList, context, setContext }) => {
433
439
  }, [revalidateCache, routeList]),
434
440
  revalidateCache,
435
441
  isCacheItemFresh,
436
- isLoading,
437
- setIsLoading
442
+ loaderStateRef
438
443
  };
439
444
  };
440
445
  //#endregion
441
446
  //#region components/RouterProvider.tsx
442
447
  var RouterProvider = ({ children, routeList, context: initialContext = {} }) => {
443
448
  const [context, setContext] = useState(initialContext);
444
- const { prefetchLoader, revalidateCache, isCacheItemFresh, setIsLoading, isLoading } = useLoader({
449
+ const { prefetchLoader, revalidateCache, isCacheItemFresh, loaderStateRef } = useLoader({
445
450
  routeList,
446
451
  context,
447
452
  setContext
448
453
  });
449
- const { blockerState, updateLocation, updateBlockedRoute, routeItemData, setSearch, restoreScroll, currentLoaderFallback } = useHandleNavigation({
454
+ const { blockerState, updateLocation, updateBlockedRoute, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState } = useHandleNavigation({
450
455
  routeList,
451
456
  context,
452
457
  setContext,
453
458
  revalidateCache,
454
459
  isCacheItemFresh,
455
- setIsLoading
460
+ loaderStateRef
456
461
  });
457
462
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
458
463
  ...useMemo(() => ({
459
- setSearch,
460
464
  updateLocation,
461
465
  prefetchLoader,
462
466
  updateBlockedRoute,
@@ -466,18 +470,19 @@ var RouterProvider = ({ children, routeList, context: initialContext = {} }) =>
466
470
  routeItemData,
467
471
  restoreScroll,
468
472
  currentLoaderFallback,
469
- isLoading
473
+ isLoading,
474
+ loaderState
470
475
  }), [
471
476
  blockerState,
472
477
  context,
473
478
  prefetchLoader,
474
479
  routeItemData,
475
- setSearch,
476
480
  updateBlockedRoute,
477
481
  updateLocation,
478
482
  currentLoaderFallback,
479
483
  restoreScroll,
480
- isLoading
484
+ isLoading,
485
+ loaderState
481
486
  ]),
482
487
  children
483
488
  });
@@ -543,14 +548,6 @@ var usePreserveScroll = (preserveScroll) => {
543
548
  ]);
544
549
  };
545
550
  //#endregion
546
- //#region hooks/useSetIsAnimated.ts
547
- var useSetIsAnimated = (isAnimated = false, showFallbackIfAnimated) => {
548
- useEffect(() => routerConfig.configure({
549
- isAnimated,
550
- showFallbackIfAnimated
551
- }), [isAnimated]);
552
- };
553
- //#endregion
554
551
  //#region components/Spinner.tsx
555
552
  var Spinner = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cr-spinner" });
556
553
  //#endregion
@@ -560,12 +557,22 @@ var renderElement = (Component) => {
560
557
  return typeof Component === "function" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, {}) : Component;
561
558
  };
562
559
  //#endregion
560
+ //#region hooks/useSetRouterConfig.ts
561
+ var useSetRouterConfig = (routerProps) => {
562
+ useEffect(() => routerConfig.configure(routerProps), [routerProps]);
563
+ };
564
+ //#endregion
563
565
  //#region components/Router.tsx
564
- var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true, showFallbackIfAnimated = false }) => {
565
- const { routeItemData: { routeItem, loaderState }, currentLoaderFallback, isLoading } = useNavigationState();
566
+ var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true, showFallbackIfAnimated = false, prefetch = "hover", hoverPrefetchDelay = 150 }) => {
567
+ const { routeItemData: { routeItem }, loaderState, currentLoaderFallback, isLoading } = useNavigationState();
566
568
  usePreserveScroll(preserveScroll);
567
569
  useApplyCustomAnimation(animationDuration);
568
- useSetIsAnimated(isAnimated, showFallbackIfAnimated);
570
+ useSetRouterConfig({
571
+ isAnimated,
572
+ showFallbackIfAnimated,
573
+ prefetch,
574
+ hoverPrefetchDelay
575
+ });
569
576
  const showErrorElement = !isLoading && Boolean(loaderState.loaderError || loaderState.beforeLoadError);
570
577
  const showSpinner = spinner && isAnimated && isLoading;
571
578
  const loadingContent = !showErrorElement && isLoading;
@@ -598,31 +605,63 @@ var useNavigate = () => {
598
605
  };
599
606
  //#endregion
600
607
  //#region components/Link.tsx
601
- var STANDARD_DELAY = 150;
602
- var Link = ({ children, to, prefetch = true, prefetchDelay = STANDARD_DELAY }) => {
608
+ var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }) => {
609
+ const { prefetch: configPrefetch, hoverPrefetchDelay: configPrefetchDelay } = routerConfig;
610
+ const prefetch = prefetchLink || configPrefetch;
611
+ const prefetchDelay = hoverPrefetchDelay ?? configPrefetchDelay;
603
612
  const { prefetchLoader } = useRouterActions();
604
613
  const navigate = useNavigate();
605
614
  const timeout = useRef(0);
615
+ const ref = useRef(null);
616
+ const onMouseEnter = useCallback(() => {
617
+ if (prefetch !== "hover" || !prefetchDelay) return;
618
+ if (timeout.current) clearTimeout(timeout.current);
619
+ timeout.current = window.setTimeout(() => prefetchLoader(to), prefetchDelay);
620
+ }, [
621
+ prefetch,
622
+ prefetchDelay,
623
+ prefetchLoader,
624
+ to
625
+ ]);
626
+ const onMouseLeave = useCallback(() => {
627
+ if (prefetch !== "hover" || !prefetchDelay) return;
628
+ if (timeout.current) {
629
+ clearTimeout(timeout.current);
630
+ timeout.current = 0;
631
+ }
632
+ }, [prefetch, prefetchDelay]);
633
+ useEffect(() => {
634
+ if (prefetch !== "render") return;
635
+ (async () => {
636
+ await prefetchLoader(to);
637
+ })();
638
+ }, [
639
+ prefetch,
640
+ prefetchLoader,
641
+ to
642
+ ]);
643
+ useEffect(() => {
644
+ if (prefetch !== "viewport") return;
645
+ const element = ref.current;
646
+ const observer = new IntersectionObserver(async () => {
647
+ await prefetchLoader(to);
648
+ observer.disconnect();
649
+ });
650
+ if (element) observer.observe(element);
651
+ return () => {
652
+ if (element) observer.disconnect();
653
+ };
654
+ }, [
655
+ prefetch,
656
+ prefetchLoader,
657
+ to
658
+ ]);
606
659
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
660
+ ref,
607
661
  style: { cursor: "pointer" },
608
662
  onClick: () => navigate(to),
609
- onMouseEnter: useCallback(() => {
610
- if (!prefetch || !prefetchDelay) return;
611
- if (timeout.current) clearTimeout(timeout.current);
612
- timeout.current = window.setTimeout(() => prefetchLoader(to), prefetchDelay);
613
- }, [
614
- prefetch,
615
- prefetchDelay,
616
- prefetchLoader,
617
- to
618
- ]),
619
- onMouseLeave: useCallback(() => {
620
- if (!prefetch || !prefetchDelay) return;
621
- if (timeout.current) {
622
- clearTimeout(timeout.current);
623
- timeout.current = 0;
624
- }
625
- }, [prefetch, prefetchDelay]),
663
+ onMouseEnter,
664
+ onMouseLeave,
626
665
  children
627
666
  });
628
667
  };
@@ -639,7 +678,7 @@ var useParams = () => {
639
678
  //#endregion
640
679
  //#region hooks/useLoaderState.ts
641
680
  var useLoaderState = () => {
642
- const { routeItemData: { loaderState } } = useNavigationState();
681
+ const { loaderState } = useNavigationState();
643
682
  return loaderState;
644
683
  };
645
684
  //#endregion
@@ -687,12 +726,40 @@ var useRouterContext = () => {
687
726
  };
688
727
  };
689
728
  //#endregion
729
+ //#region hooks/useSearch.ts
730
+ var notify = () => {
731
+ window.dispatchEvent(new Event("locationChange"));
732
+ };
733
+ var patchHistory = () => {
734
+ const push = history.pushState;
735
+ const replace = history.replaceState;
736
+ history.pushState = function(...args) {
737
+ const result = push.apply(this, args);
738
+ notify();
739
+ return result;
740
+ };
741
+ history.replaceState = function(...args) {
742
+ const result = replace.apply(this, args);
743
+ notify();
744
+ return result;
745
+ };
746
+ window.addEventListener("popstate", notify);
747
+ };
748
+ var subscribe = (onChange) => {
749
+ window.addEventListener("locationChange", onChange);
750
+ return () => window.removeEventListener("locationChange", onChange);
751
+ };
752
+ var getSnapshot = () => window.location.search;
753
+ var useSearch = () => {
754
+ useEffect(patchHistory, []);
755
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
756
+ };
757
+ //#endregion
690
758
  //#region hooks/useSearchParams.ts
691
759
  var useSearchParams = () => {
692
- const { routeItemData: { location } } = useNavigationState();
693
- const { setSearch } = useRouterActions();
694
- const locationRef = useLatest(location);
695
- const searchString = useMemo(() => location.search ? location.search.replace("?", "") : location.pathname.split("?")?.[1] ?? "", [location.pathname, location.search]);
760
+ const search = useSearch();
761
+ const searchRef = useLatest(search);
762
+ const searchString = search ? search.replace("?", "") : window.location.pathname.split("?")?.[1] ?? "";
696
763
  const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
697
764
  const getSearchParams = useCallback((param) => {
698
765
  const allValues = searchParams.getAll(param);
@@ -700,24 +767,22 @@ var useSearchParams = () => {
700
767
  }, [searchParams]);
701
768
  const navigateWithSearchParams = useCallback((params) => {
702
769
  const newSearch = params.toString();
703
- const { pathname } = locationRef.current;
770
+ const { pathname } = window.location;
704
771
  const search = newSearch ? `?${newSearch}` : "";
705
- setSearch(search);
706
772
  history.replaceState(null, "", pathname + search);
707
- }, [locationRef, setSearch]);
773
+ }, []);
708
774
  return {
709
775
  searchParams,
710
776
  getSearchParams,
711
777
  setSearchParams: useCallback((param, value) => {
712
- const search = locationRef.current.search || "";
713
- const currentParams = new URLSearchParams(search);
778
+ const currentParams = new URLSearchParams(searchRef.current);
714
779
  if (typeof param === "string" && value !== void 0) {
715
780
  currentParams.delete(param);
716
781
  (Array.isArray(value) ? value : [value]).forEach((v) => currentParams.append(param, v));
717
782
  navigateWithSearchParams(currentParams);
718
783
  } else if (typeof param === "function") navigateWithSearchParams(param(currentParams));
719
784
  else throw new Error("useSearchParams first argument must be either function or string");
720
- }, [locationRef, navigateWithSearchParams])
785
+ }, [navigateWithSearchParams, searchRef])
721
786
  };
722
787
  };
723
788
  //#endregion
@@ -3,5 +3,5 @@ import { type ActionsContextValue, type DataContextValue, type NavigationContext
3
3
  type ProviderProps = NavigationContextValue & ActionsContextValue & DataContextValue & {
4
4
  children: ReactNode;
5
5
  };
6
- export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation, setSearch, prefetchLoader, blockerState, routeItemData, restoreScroll, currentLoaderFallback, isLoading, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
6
+ export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation, prefetchLoader, blockerState, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
7
7
  export {};
@@ -1,4 +1,4 @@
1
- import type { ComponentType, Dispatch, ReactElement, RefObject, SetStateAction } from 'react';
1
+ import type { ComponentType, Dispatch, ReactElement, SetStateAction } from 'react';
2
2
  export type LazyComponent = () => Promise<{
3
3
  default: ComponentType<unknown>;
4
4
  }>;
@@ -48,7 +48,6 @@ export type UpdateBlockedRouteProps = {
48
48
  export type RevalidateCacheArgs = {
49
49
  pathname: string;
50
50
  routeItem?: RouteItem;
51
- loaderState?: RefObject<LoaderState>;
52
51
  };
53
52
  export type LoaderState = {
54
53
  data: unknown;
@@ -62,5 +61,13 @@ export type AdapterType<T> = {
62
61
  export type RouteItemData = {
63
62
  location: Location;
64
63
  routeItem: RouteItem | undefined;
65
- loaderState: LoaderState;
64
+ };
65
+ export type RouterProps = {
66
+ isAnimated?: boolean;
67
+ animationDuration?: number;
68
+ spinner?: boolean;
69
+ preserveScroll?: boolean;
70
+ showFallbackIfAnimated?: boolean;
71
+ prefetch?: 'hover' | 'render' | 'viewport' | 'none';
72
+ hoverPrefetchDelay?: number;
66
73
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.5.7",
3
+ "version": "1.6.0",
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 useSetIsAnimated: (isAnimated: boolean | undefined, showFallbackIfAnimated: boolean) => void;