clear-react-router 1.5.8 → 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,9 +1,10 @@
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
10
  updateLocation(route: Location): Promise<void>;
@@ -1,22 +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
19
  restoreScroll: () => void;
20
20
  currentLoaderFallback: import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | (() => import("react").ReactElement) | undefined;
21
+ isLoading: boolean;
22
+ loaderState: LoaderState;
21
23
  };
22
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,2 @@
1
+ import { RouterProps } from '../types/global';
2
+ export declare const useSetRouterConfig: (routerProps: RouterProps) => void;
package/dist/index.js CHANGED
@@ -45,7 +45,7 @@ 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, 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
51
  updateLocation,
@@ -61,7 +61,8 @@ var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocat
61
61
  blockerState,
62
62
  routeItemData,
63
63
  currentLoaderFallback,
64
- isLoading
64
+ isLoading,
65
+ loaderState
65
66
  },
66
67
  children
67
68
  })
@@ -78,6 +79,9 @@ var useLatest = (value) => {
78
79
  return ref;
79
80
  };
80
81
  //#endregion
82
+ //#region constants.ts
83
+ var emptyLoaderState = {};
84
+ //#endregion
81
85
  //#region \0@oxc-project+runtime@0.133.0/helpers/esm/typeof.js
82
86
  function _typeof(o) {
83
87
  "@babel/helpers - typeof";
@@ -121,6 +125,8 @@ var RouterConfig = class {
121
125
  constructor() {
122
126
  _defineProperty(this, "isAnimated", false);
123
127
  _defineProperty(this, "showFallbackIfAnimated", false);
128
+ _defineProperty(this, "prefetch", "hover");
129
+ _defineProperty(this, "hoverPrefetchDelay", 150);
124
130
  }
125
131
  configure(config) {
126
132
  Object.assign(this, config);
@@ -181,20 +187,20 @@ var comparePaths = (el, pathname) => {
181
187
  //#endregion
182
188
  //#region hooks/useHandleNavigation.ts
183
189
  var ALL_LOCATIONS = "*";
184
- var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, isCacheItemFresh, setIsLoading }) => {
190
+ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef }) => {
185
191
  const { isAnimated, showFallbackIfAnimated: showFallback } = routerConfig;
192
+ const [isLoading, setIsLoading] = useState(false);
186
193
  const [blockedRoute, setBlockedRoute] = useState({
187
194
  from: "",
188
195
  to: ""
189
196
  });
190
197
  const [routeItemData, setRouteItemData] = useState({
191
- location: {},
192
198
  routeItem: void 0,
193
- loaderState: {}
199
+ location: {}
194
200
  });
195
201
  const [scrollMap, setScrollMap] = useState({});
196
202
  const [currentLoaderFallback, setCurrentLoaderFallback] = useState();
197
- const loaderState = useRef({});
203
+ const [loaderState, setLoaderState] = useState(emptyLoaderState);
198
204
  const prevPathname = useRef("");
199
205
  const navigationSeq = useRef(0);
200
206
  const scrollMapRef = useLatest(scrollMap);
@@ -209,17 +215,17 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
209
215
  }, [scrollMapRef]);
210
216
  const navigation = useCallback((nextLocation, routeItem) => {
211
217
  setRouteItemData({
212
- location: nextLocation,
213
218
  routeItem,
214
- loaderState: loaderState.current
219
+ location: nextLocation
215
220
  });
221
+ setLoaderState(loaderStateRef.current);
216
222
  setIsLoading(false);
217
223
  setCurrentLoaderFallback(void 0);
218
224
  prevPathname.current = nextLocation.pathname;
219
225
  const fullPath = nextLocation.search ? `${nextLocation.pathname}${nextLocation.search}` : nextLocation.pathname;
220
226
  if (fullPath === window.location.pathname + window.location.search) return;
221
227
  history.pushState(null, "", fullPath);
222
- }, []);
228
+ }, [loaderStateRef]);
223
229
  const transitionedNavigation = useCallback((nextLocation, routeItem) => {
224
230
  if (!isAnimated) {
225
231
  navigation(nextLocation, routeItem);
@@ -234,7 +240,7 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
234
240
  const navigationHandler = useCallback(async (nextLocation) => {
235
241
  navigationSeq.current = navigationSeq.current + 1;
236
242
  const seq = navigationSeq.current;
237
- loaderState.current = {};
243
+ loaderStateRef.current = emptyLoaderState;
238
244
  const nextItem = routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, nextLocation.pathname));
239
245
  const params = getParamsObject({
240
246
  params: nextItem?.params,
@@ -248,13 +254,13 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
248
254
  params,
249
255
  setContext
250
256
  });
251
- loaderState.current = {
252
- ...loaderState.current,
257
+ loaderStateRef.current = {
258
+ ...loaderStateRef.current,
253
259
  beforeLoadError: null
254
260
  };
255
261
  } catch (error) {
256
- loaderState.current = {
257
- ...loaderState.current,
262
+ loaderStateRef.current = {
263
+ ...loaderStateRef.current,
258
264
  beforeLoadError: error
259
265
  };
260
266
  return transitionedNavigation(nextLocation, nextItem);
@@ -272,11 +278,13 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
272
278
  routeItem: nextItem,
273
279
  pathname: nextLocation.pathname
274
280
  }) || isAnimated && !showFallback ? void 0 : nextItem?.loaderFallback);
275
- await revalidateCache({
276
- routeItem: nextItem,
277
- loaderState,
278
- pathname: nextLocation.pathname
279
- });
281
+ if (nextItem?.loader) {
282
+ setIsLoading(true);
283
+ await revalidateCache({
284
+ routeItem: nextItem,
285
+ pathname: nextLocation.pathname
286
+ });
287
+ }
280
288
  if (seq !== navigationSeq.current) return;
281
289
  transitionedNavigation(nextLocation, nextItem);
282
290
  if (nextItem?.afterLoad) await nextItem.afterLoad({
@@ -292,7 +300,8 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
292
300
  setContext,
293
301
  isCacheItemFresh,
294
302
  isAnimated,
295
- showFallback
303
+ showFallback,
304
+ loaderStateRef
296
305
  ]);
297
306
  const setNextLocationRef = useLatest(navigationHandler);
298
307
  const updateBlockedRoute = useCallback(({ type, payload = "" }) => setBlockedRoute((prevState) => {
@@ -348,67 +357,73 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
348
357
  updateBlockedRoute,
349
358
  routeItemData,
350
359
  restoreScroll,
351
- currentLoaderFallback
360
+ currentLoaderFallback,
361
+ isLoading,
362
+ loaderState
352
363
  };
353
364
  };
354
365
  //#endregion
355
366
  //#region hooks/useLoader.ts
356
367
  var useLoader = ({ routeList, context, setContext }) => {
357
- const [isLoading, setIsLoading] = useState(false);
358
- const cacheTimestampsRef = useRef({});
359
- const loaderCacheRef = useRef({});
368
+ const timestampMapRef = useRef({});
369
+ const loaderMapRef = useRef({});
370
+ const loaderStateRef = useRef(emptyLoaderState);
371
+ const loadingPromises = useRef(/* @__PURE__ */ new Map());
360
372
  const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
361
373
  if (!routeItem) return true;
362
- const currentCacheTimestamp = cacheTimestampsRef.current[pathname];
374
+ const currentCacheTimestamp = timestampMapRef.current[pathname];
363
375
  if (!currentCacheTimestamp) return false;
364
376
  if (!routeItem.staleTime) return true;
365
377
  return Date.now() - currentCacheTimestamp < routeItem.staleTime;
366
378
  }, []);
367
- const revalidateCache = useCallback(async ({ routeItem, loaderState, pathname }) => {
368
- if (!routeItem?.loader) {
369
- if (!loaderState) return;
370
- if (!routeItem?.beforeLoad) loaderState.current = {};
371
- return;
372
- }
379
+ const revalidateCache = useCallback(async ({ routeItem, pathname }) => {
380
+ if (!routeItem?.loader) return;
381
+ if (loadingPromises.current.has(pathname)) return loadingPromises.current.get(pathname);
373
382
  if (isCacheItemFresh({
374
383
  routeItem,
375
384
  pathname
376
385
  })) {
377
- if (loaderState) loaderState.current = loaderCacheRef.current[pathname];
386
+ loaderStateRef.current = loaderMapRef.current[pathname];
378
387
  return;
379
388
  }
380
- if (loaderState) setIsLoading(true);
381
- try {
382
- const params = getParamsObject({
383
- params: routeItem.params,
384
- pathname
385
- });
386
- const result = await routeItem?.loader({
387
- params,
388
- context,
389
- setContext
390
- });
391
- cacheTimestampsRef.current = {
392
- ...cacheTimestampsRef.current,
393
- [pathname]: Date.now()
394
- };
395
- loaderCacheRef.current[pathname] = {
396
- data: result,
397
- loaderError: null,
398
- beforeLoadError: null
399
- };
400
- if (loaderState) loaderState.current = {
401
- ...loaderState?.current,
402
- data: result,
403
- loaderError: null
404
- };
405
- } catch (error) {
406
- if (loaderState) loaderState.current = {
407
- ...loaderState?.current,
408
- data: null,
409
- loaderError: error
410
- };
411
- }
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;
412
427
  }, [
413
428
  context,
414
429
  isCacheItemFresh,
@@ -424,26 +439,25 @@ var useLoader = ({ routeList, context, setContext }) => {
424
439
  }, [revalidateCache, routeList]),
425
440
  revalidateCache,
426
441
  isCacheItemFresh,
427
- isLoading,
428
- setIsLoading
442
+ loaderStateRef
429
443
  };
430
444
  };
431
445
  //#endregion
432
446
  //#region components/RouterProvider.tsx
433
447
  var RouterProvider = ({ children, routeList, context: initialContext = {} }) => {
434
448
  const [context, setContext] = useState(initialContext);
435
- const { prefetchLoader, revalidateCache, isCacheItemFresh, setIsLoading, isLoading } = useLoader({
449
+ const { prefetchLoader, revalidateCache, isCacheItemFresh, loaderStateRef } = useLoader({
436
450
  routeList,
437
451
  context,
438
452
  setContext
439
453
  });
440
- const { blockerState, updateLocation, updateBlockedRoute, routeItemData, restoreScroll, currentLoaderFallback } = useHandleNavigation({
454
+ const { blockerState, updateLocation, updateBlockedRoute, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState } = useHandleNavigation({
441
455
  routeList,
442
456
  context,
443
457
  setContext,
444
458
  revalidateCache,
445
459
  isCacheItemFresh,
446
- setIsLoading
460
+ loaderStateRef
447
461
  });
448
462
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
449
463
  ...useMemo(() => ({
@@ -456,7 +470,8 @@ var RouterProvider = ({ children, routeList, context: initialContext = {} }) =>
456
470
  routeItemData,
457
471
  restoreScroll,
458
472
  currentLoaderFallback,
459
- isLoading
473
+ isLoading,
474
+ loaderState
460
475
  }), [
461
476
  blockerState,
462
477
  context,
@@ -466,7 +481,8 @@ var RouterProvider = ({ children, routeList, context: initialContext = {} }) =>
466
481
  updateLocation,
467
482
  currentLoaderFallback,
468
483
  restoreScroll,
469
- isLoading
484
+ isLoading,
485
+ loaderState
470
486
  ]),
471
487
  children
472
488
  });
@@ -532,14 +548,6 @@ var usePreserveScroll = (preserveScroll) => {
532
548
  ]);
533
549
  };
534
550
  //#endregion
535
- //#region hooks/useSetIsAnimated.ts
536
- var useSetIsAnimated = (isAnimated = false, showFallbackIfAnimated) => {
537
- useEffect(() => routerConfig.configure({
538
- isAnimated,
539
- showFallbackIfAnimated
540
- }), [isAnimated]);
541
- };
542
- //#endregion
543
551
  //#region components/Spinner.tsx
544
552
  var Spinner = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cr-spinner" });
545
553
  //#endregion
@@ -549,12 +557,22 @@ var renderElement = (Component) => {
549
557
  return typeof Component === "function" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, {}) : Component;
550
558
  };
551
559
  //#endregion
560
+ //#region hooks/useSetRouterConfig.ts
561
+ var useSetRouterConfig = (routerProps) => {
562
+ useEffect(() => routerConfig.configure(routerProps), [routerProps]);
563
+ };
564
+ //#endregion
552
565
  //#region components/Router.tsx
553
- var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true, showFallbackIfAnimated = false }) => {
554
- 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();
555
568
  usePreserveScroll(preserveScroll);
556
569
  useApplyCustomAnimation(animationDuration);
557
- useSetIsAnimated(isAnimated, showFallbackIfAnimated);
570
+ useSetRouterConfig({
571
+ isAnimated,
572
+ showFallbackIfAnimated,
573
+ prefetch,
574
+ hoverPrefetchDelay
575
+ });
558
576
  const showErrorElement = !isLoading && Boolean(loaderState.loaderError || loaderState.beforeLoadError);
559
577
  const showSpinner = spinner && isAnimated && isLoading;
560
578
  const loadingContent = !showErrorElement && isLoading;
@@ -587,31 +605,63 @@ var useNavigate = () => {
587
605
  };
588
606
  //#endregion
589
607
  //#region components/Link.tsx
590
- var STANDARD_DELAY = 150;
591
- 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;
592
612
  const { prefetchLoader } = useRouterActions();
593
613
  const navigate = useNavigate();
594
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
+ ]);
595
659
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
660
+ ref,
596
661
  style: { cursor: "pointer" },
597
662
  onClick: () => navigate(to),
598
- onMouseEnter: useCallback(() => {
599
- if (!prefetch || !prefetchDelay) return;
600
- if (timeout.current) clearTimeout(timeout.current);
601
- timeout.current = window.setTimeout(() => prefetchLoader(to), prefetchDelay);
602
- }, [
603
- prefetch,
604
- prefetchDelay,
605
- prefetchLoader,
606
- to
607
- ]),
608
- onMouseLeave: useCallback(() => {
609
- if (!prefetch || !prefetchDelay) return;
610
- if (timeout.current) {
611
- clearTimeout(timeout.current);
612
- timeout.current = 0;
613
- }
614
- }, [prefetch, prefetchDelay]),
663
+ onMouseEnter,
664
+ onMouseLeave,
615
665
  children
616
666
  });
617
667
  };
@@ -628,7 +678,7 @@ var useParams = () => {
628
678
  //#endregion
629
679
  //#region hooks/useLoaderState.ts
630
680
  var useLoaderState = () => {
631
- const { routeItemData: { loaderState } } = useNavigationState();
681
+ const { loaderState } = useNavigationState();
632
682
  return loaderState;
633
683
  };
634
684
  //#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, 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.8",
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;