clear-react-router 2.0.5 → 2.0.7

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
@@ -46,10 +46,10 @@ It provides first-class support for:
46
46
  | `maxCacheSize` | `number \| undefined` | 60 for mobile, 150 for desktop | Maximum number of cached loader entries. Once the limit is reached, the least recently used entries are evicted |
47
47
  | `isAnimated` | `boolean \| undefined` | `false` | Enable smooth page fade transitions |
48
48
  | `animationDuration` | `number` | `optional` | Animation duration in milliseconds (browser default is used if not set) |
49
- | `spinner` | `boolean \| undefined` | `true` | Show a small spinner in the corner while loading data (only when `isAnimated` is enabled) |
49
+ | `optimisticSpinner` | `boolean \| undefined` | `true` | Show a small spinner in the corner while optimistic route data revalidates in the background |
50
50
  | `context` | `object` | `{}` | Initial context (user, theme, etc.) |
51
51
  | `errorBoundary` | `ComponentType<{ children: ReactNode }>` | `undefined` | Custom error boundary component for catching render errors in route components |
52
- | `showFallbackOnAnimation` | `boolean \| undefined` | `false` | Show `loaderFallback` even when `isAnimated` is `true` (instead of spinner) |
52
+ | `defaultMinLoaderDuration` | `number \| undefined` | `0` | Default minimum time the loader fallback stays visible, to avoid flickering |
53
53
  | `defaultLoaderFallback` | `ReactElement \| () => ReactElement` | `optional` | Default loading fallback for every route loader |
54
54
  | `defaultErrorElement` | `ReactElement \| () => ReactElement` | `optional` | Default error fallback for every route |
55
55
  | `defaultRetry` | `number \| { count: number; delay: number }` | `optional` | Default cache revalidation retry policy for all routes |
@@ -63,14 +63,6 @@ It provides first-class support for:
63
63
 
64
64
  > **Note:** Global lifecycle hooks wrap every route navigation. The global `defaultBeforeLoad` runs **before** the route-specific beforeLoad, while the global `defaultAfterLoad` runs **after** the route-specific afterLoad.
65
65
 
66
- ```tsx
67
- <div>
68
- <Navbar />
69
- <Router routes={routes} spinner={false} isAnimated /> {/* disable the spinner */}
70
- </div>
71
- ```
72
- > **Note:** When `isAnimated` is enabled, `loaderFallback` is not shown. Instead, a small spinner appears (if `spinner={true}`). On the initial page load, however, the route's loaderFallback is rendered if available.
73
-
74
66
  ### `createRouter(routes)`
75
67
 
76
68
  Normalizes route configuration. Extracts dynamic params, builds nested paths.
@@ -82,6 +74,7 @@ Normalizes route configuration. Extracts dynamic params, builds nested paths.
82
74
  | `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 |
83
75
  | `loader` | `({ params, context, setContext, searchParams, signal }) => Promise<unknown>` | Fetch data using route params, search params, abort controller signal and context. Can update context via `setContext` |
84
76
  | `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` |
77
+ | `minLoaderDuration` | `number \| undefined` | `undefined` | Minimum time the loader fallback stays visible, to avoid flickering |
85
78
  | `fallback` | `ReactElement \| () => ReactElement` | Loading fallback (for lazy loading) |
86
79
  | `loaderFallback` | `ReactElement \| () => ReactElement` | Loading fallback for the route's `loader`. Overrides the global `defaultLoaderFallback` set in `Router` |
87
80
  | `retry` | `number \| { count: number; delay: number }` | Overrides the global cache revalidation retry policy for this route |
@@ -816,8 +809,6 @@ Clear Router supports smooth page transitions using the native View Transitions
816
809
  ## How It Works
817
810
 
818
811
  - **Data loads first** — All `loader` and `beforeLoad` hooks complete before animation starts
819
- - **No `loaderFallback`** — The `loaderFallback` is not shown during animated transitions
820
- - **Subtle spinner** — A small spinner appears in the top-left corner while data is loading and `spinner` prop of `Router` component is on, so users know the app is responsive
821
812
  - **Native API** — Uses `document.startViewTransition` for smooth, hardware-accelerated animations
822
813
 
823
814
  ## Browser Support
@@ -1,2 +1,2 @@
1
1
  import { RouterProps } from '../types';
2
- export declare const Router: ({ routes, defaultBeforeLoad, defaultAfterLoad, animationDuration, defaultLoaderFallback, defaultErrorElement, defaultRetry, defaultStaleTime, context: initialContext, isAnimated, spinner, defaultPreserveScroll, showFallbackOnAnimation, maxCacheSize, defaultPrefetch, defaultHoverPrefetchDelay, errorBoundary: ErrorBoundary, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
2
+ export declare const Router: ({ routes, defaultBeforeLoad, defaultAfterLoad, animationDuration, defaultLoaderFallback, defaultErrorElement, defaultRetry, defaultStaleTime, context: initialContext, isAnimated, optimisticSpinner, defaultPreserveScroll, defaultMinLoaderDuration, maxCacheSize, defaultPrefetch, defaultHoverPrefetchDelay, errorBoundary: ErrorBoundary, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -10,6 +10,7 @@ declare class RouterConfig {
10
10
  defaultRetry?: RouterProps['defaultRetry'];
11
11
  defaultStaleTime?: RouterProps['defaultStaleTime'];
12
12
  defaultPreserveScroll?: RouterProps['defaultPreserveScroll'];
13
+ defaultMinLoaderDuration?: RouterProps['defaultMinLoaderDuration'];
13
14
  configure(config: Partial<RouterConfig>): void;
14
15
  }
15
16
  export declare const routerConfig: RouterConfig;
@@ -1,2 +1 @@
1
- import { LoaderState } from '../types';
2
- export declare const useLoaderState: <T>() => LoaderState<T>;
1
+ export declare const useLoaderState: <T>() => import("../types").LoaderState<T>;
package/dist/index.js CHANGED
@@ -30,12 +30,13 @@ var useGlobalState = ({ subscribe, getState, setState }) => {
30
30
  };
31
31
  //#endregion
32
32
  //#region utils/commitState.ts
33
- var createCommitState = ({ routeItemDataState, pendingState }) => (nextLocation, routeItem) => {
33
+ var createCommitState = ({ routeItemDataState, pendingState, isOptimisticLoading }) => (nextLocation, routeItem) => {
34
34
  routeItemDataState.setState({
35
35
  routeItem,
36
36
  location: nextLocation
37
37
  });
38
38
  pendingState.setState(void 0);
39
+ isOptimisticLoading.setState(false);
39
40
  const fullPath = nextLocation.search ? `${nextLocation.pathname}${nextLocation.search}` : nextLocation.pathname;
40
41
  if (fullPath === window.location.pathname + window.location.search) return;
41
42
  history.pushState(null, "", fullPath);
@@ -95,6 +96,7 @@ var RouterConfig = class {
95
96
  _defineProperty(this, "defaultRetry", void 0);
96
97
  _defineProperty(this, "defaultStaleTime", void 0);
97
98
  _defineProperty(this, "defaultPreserveScroll", void 0);
99
+ _defineProperty(this, "defaultMinLoaderDuration", void 0);
98
100
  }
99
101
  configure(config) {
100
102
  Object.assign(this, config);
@@ -103,13 +105,12 @@ var RouterConfig = class {
103
105
  var routerConfig = new RouterConfig();
104
106
  //#endregion
105
107
  //#region utils/commitNavigation.ts
106
- var createCommitNavigation = (navigationExecutor, routeItemDataState) => (nextLocation, routeItem) => {
107
- const isFirstLoad = !routeItemDataState.getState().location.pathname;
108
- if (!routerConfig.isAnimated || isFirstLoad) return navigationExecutor(nextLocation, routeItem);
108
+ var commitNavigation = (callback) => {
109
+ if (!routerConfig.isAnimated) return callback();
109
110
  try {
110
- document.startViewTransition(() => navigationExecutor(nextLocation, routeItem));
111
+ document.startViewTransition(callback);
111
112
  } catch {
112
- navigationExecutor(nextLocation, routeItem);
113
+ callback();
113
114
  }
114
115
  };
115
116
  //#endregion
@@ -133,7 +134,7 @@ var createIsCacheItemFresh = (loaderMap) => (path) => {
133
134
  * LICENSE file in the root directory of this source tree.
134
135
  */
135
136
  var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((exports) => {
136
- var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
137
+ var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element");
137
138
  function jsxProd(type, config, maybeKey) {
138
139
  var key = null;
139
140
  void 0 !== maybeKey && (key = "" + maybeKey);
@@ -151,7 +152,6 @@ var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((expor
151
152
  props: maybeKey
152
153
  };
153
154
  }
154
- exports.Fragment = REACT_FRAGMENT_TYPE;
155
155
  exports.jsx = jsxProd;
156
156
  exports.jsxs = jsxProd;
157
157
  }));
@@ -215,6 +215,7 @@ var isMobile = () => {
215
215
  const isSmallScreen = window.matchMedia("(max-width: 768px)").matches;
216
216
  return hasCoarsePointer && isSmallScreen;
217
217
  };
218
+ var sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
218
219
  var findRoute = (pathname, includeAll) => {
219
220
  if (includeAll) return routerConfig.routes.find((el) => el.path === "*" || comparePaths(el, pathname));
220
221
  return routerConfig.routes.find((el) => comparePaths(el, pathname));
@@ -225,20 +226,21 @@ var createNavigate = (routerState, revalidateCache) => {
225
226
  let navigationSeq = 0;
226
227
  let interval = 0;
227
228
  let abortController = null;
228
- const { loaderStateRef, scrollMapState, pendingState, contextState, loaderMap, routeItemDataState } = routerState;
229
- const commitNavigation = createCommitNavigation(createCommitState(routerState), routeItemDataState);
229
+ const { loaderState, scrollMapState, pendingState, contextState, loaderMap, routeItemDataState, isOptimisticLoading } = routerState;
230
+ const navigationExecutor = createCommitState(routerState);
230
231
  const isCacheItemFresh = createIsCacheItemFresh(loaderMap);
231
232
  const createSignal = () => {
232
233
  abortController?.abort();
233
234
  abortController = new AbortController();
234
235
  return abortController.signal;
235
236
  };
237
+ const getPath = (nextLocation) => `${nextLocation.pathname}${nextLocation.search ?? ""}`;
236
238
  const getContext = () => ({
237
239
  context: contextState.getState(),
238
240
  setContext: contextState.setState
239
241
  });
240
242
  const routeResolve = (location) => {
241
- loaderStateRef.set(emptyLoaderState);
243
+ loaderState.setState(emptyLoaderState);
242
244
  const nextItem = findRoute(location.pathname, true);
243
245
  return {
244
246
  nextItem,
@@ -255,12 +257,12 @@ var createNavigate = (routerState, revalidateCache) => {
255
257
  redirect,
256
258
  params
257
259
  });
258
- loaderStateRef.set((prev) => ({
260
+ loaderState.setState((prev) => ({
259
261
  ...prev,
260
262
  beforeLoadError: null
261
263
  }));
262
264
  } catch (error) {
263
- loaderStateRef.set((prev) => ({
265
+ loaderState.setState((prev) => ({
264
266
  ...prev,
265
267
  beforeLoadError: error
266
268
  }));
@@ -279,44 +281,51 @@ var createNavigate = (routerState, revalidateCache) => {
279
281
  [prevPathname]: scrollPosition
280
282
  };
281
283
  });
282
- const path = `${location.pathname}${location.search}`;
284
+ const path = getPath(location);
283
285
  if (routeItem?.optimistic && loaderMap.has(path)) {
284
286
  routeItemDataState.setState({
285
287
  routeItem,
286
288
  location
287
289
  });
290
+ isOptimisticLoading.setState(true);
288
291
  const currentLoaderState = loaderMap.get(path)?.state;
289
- if (currentLoaderState) loaderStateRef.set(currentLoaderState);
290
- } else {
291
- const pendingShouldExist = routeItem?.loader && !isCacheItemFresh(path);
292
- pendingState.setState(pendingShouldExist ? {
293
- routeItem,
294
- location
295
- } : void 0);
296
- }
292
+ if (currentLoaderState) loaderState.setState(currentLoaderState);
293
+ } else if (routeItem?.loader && !isCacheItemFresh(path)) commitNavigation(() => pendingState.setState({
294
+ routeItem,
295
+ location
296
+ }));
297
+ else pendingState.setState(void 0);
297
298
  };
298
- const polling = (routeItem, location) => {
299
+ const polling = (routeItem, nextLocation) => {
299
300
  if (!routeItem?.pollingInterval) return;
300
301
  const signal = createSignal();
301
302
  interval = window.setInterval(() => revalidateCache({
302
303
  routeItem,
303
- pathname: location.pathname,
304
- search: location.search,
304
+ pathname: nextLocation.pathname,
305
+ search: nextLocation.search,
305
306
  signal
306
307
  }), routeItem.pollingInterval);
307
308
  };
308
- const loader = async (routeItem, location, seq) => {
309
+ const getLoaderDurationPromise = (routeItem, nextLocation) => {
310
+ const minLoaderDuration = routeItem?.minLoaderDuration ?? routerConfig.defaultMinLoaderDuration ?? 0;
311
+ return minLoaderDuration && !isCacheItemFresh(getPath(nextLocation)) ? sleep(minLoaderDuration) : Promise.resolve;
312
+ };
313
+ const loader = async (routeItem, nextLocation, seq) => {
309
314
  if (!routeItem?.loader) return;
310
315
  window.clearInterval(interval);
311
316
  const signal = createSignal();
312
- await revalidateCache({
317
+ const [result] = await Promise.all([revalidateCache({
313
318
  routeItem,
314
- pathname: location.pathname,
315
- search: location.search,
319
+ pathname: nextLocation.pathname,
320
+ search: nextLocation.search,
316
321
  signal
317
- });
322
+ }), getLoaderDurationPromise(routeItem, nextLocation)]);
323
+ if (result) loaderState.setState((prev) => ({
324
+ ...prev,
325
+ ...result
326
+ }));
318
327
  if (seq !== navigationSeq) return;
319
- polling(routeItem, location);
328
+ polling(routeItem, nextLocation);
320
329
  };
321
330
  const afterLoad = async (routeItem, params) => {
322
331
  const { defaultAfterLoad } = routerConfig;
@@ -338,7 +347,7 @@ var createNavigate = (routerState, revalidateCache) => {
338
347
  prepareNavigation(nextItem, nextLocation);
339
348
  await loader(nextItem, nextLocation, seq);
340
349
  if (seq !== navigationSeq) return;
341
- commitNavigation(nextLocation, nextItem);
350
+ commitNavigation(() => navigationExecutor(nextLocation, nextItem));
342
351
  await afterLoad(nextItem, params);
343
352
  };
344
353
  return navigate;
@@ -346,7 +355,7 @@ var createNavigate = (routerState, revalidateCache) => {
346
355
  //#endregion
347
356
  //#region runtime/invalidate.ts
348
357
  var redirect = Promise.resolve;
349
- var createInvalidate = ({ routeItemDataState, loaderStateRef, loaderMap, currentLoaderState, contextState }, revalidateCache) => {
358
+ var createInvalidate = ({ routeItemDataState, loaderState, loaderMap, contextState }, revalidateCache) => {
350
359
  const invalidatePath = async (routeItem, pathname, options) => {
351
360
  const routePathname = routeItemDataState.getState().location.pathname;
352
361
  loaderMap.delete(pathname);
@@ -362,12 +371,12 @@ var createInvalidate = ({ routeItemDataState, loaderStateRef, loaderMap, current
362
371
  setContext
363
372
  });
364
373
  }
365
- loaderStateRef.set((prev) => ({
374
+ loaderState.setState((prev) => ({
366
375
  ...prev,
367
376
  beforeLoadError: null
368
377
  }));
369
378
  } catch (error) {
370
- loaderStateRef.set((prev) => ({
379
+ loaderState.setState((prev) => ({
371
380
  ...prev,
372
381
  beforeLoadError: error
373
382
  }));
@@ -376,7 +385,11 @@ var createInvalidate = ({ routeItemDataState, loaderStateRef, loaderMap, current
376
385
  routeItem,
377
386
  pathname
378
387
  });
379
- if (pathname === routePathname) currentLoaderState.setState(loaderStateRef.value);
388
+ if (result && pathname === routePathname) loaderState.setState({
389
+ data: result.data,
390
+ loaderError: result.error,
391
+ beforeLoadError: null
392
+ });
380
393
  return {
381
394
  path: pathname,
382
395
  ...result
@@ -430,16 +443,8 @@ var getRetry = (routeItem) => {
430
443
  delay: routeRetry ? routeRetry.delay : globalRetry?.delay || 0
431
444
  };
432
445
  };
433
- var sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
434
446
  var createRevalidateCache = (routerState) => {
435
- const { loaderStateRef, contextState, loaderMap, loadingPromises } = routerState;
436
- const removeStaleItems = () => {
437
- const deletedItems = [...loaderMap.entries()].filter(([, item]) => {
438
- const staleTime = item.staleTime ?? routerConfig.defaultStaleTime;
439
- return staleTime && staleTime + item.timestamp < Date.now();
440
- });
441
- if (deletedItems.length) deletedItems.forEach((item) => loaderMap.delete(item[0]));
442
- };
447
+ const { contextState, loaderMap, loadingPromises } = routerState;
443
448
  const evict = () => {
444
449
  if (loaderMap.size <= routerConfig.maxCacheSize) return;
445
450
  const oldestKey = loaderMap.keys().next().value;
@@ -456,7 +461,6 @@ var createRevalidateCache = (routerState) => {
456
461
  const revalidateCache = async ({ routeItem, pathname, search = "", signal }, retried = 0) => {
457
462
  if (!routeItem?.loader) return;
458
463
  const isCacheItemFresh = createIsCacheItemFresh(loaderMap);
459
- removeStaleItems();
460
464
  const path = `${pathname}${search}`;
461
465
  if (loadingPromises.has(path)) {
462
466
  moveItemToLastPosition(path);
@@ -464,8 +468,14 @@ var createRevalidateCache = (routerState) => {
464
468
  }
465
469
  if (isCacheItemFresh(path)) {
466
470
  const item = moveItemToLastPosition(path);
467
- if (item?.state) loaderStateRef.set(item.state);
468
- return;
471
+ if (!item?.state) return void 0;
472
+ return item.state.loaderError ? {
473
+ data: null,
474
+ error: item.state.loaderError
475
+ } : {
476
+ data: item.state.data,
477
+ error: null
478
+ };
469
479
  }
470
480
  const promise = (async () => {
471
481
  if (!routeItem?.loader) return;
@@ -482,13 +492,12 @@ var createRevalidateCache = (routerState) => {
482
492
  searchParams,
483
493
  signal: effectiveSignal
484
494
  });
485
- loaderStateRef.set((prev) => ({
486
- ...prev,
487
- data: result,
488
- loaderError: null
489
- }));
490
495
  loaderMap.set(path, {
491
- state: loaderStateRef.value,
496
+ state: {
497
+ data: result,
498
+ beforeLoadError: null,
499
+ loaderError: null
500
+ },
492
501
  timestamp: Date.now(),
493
502
  staleTime: routeItem.staleTime
494
503
  });
@@ -506,27 +515,16 @@ var createRevalidateCache = (routerState) => {
506
515
  if (retry && retry.count > retried) {
507
516
  loadingPromises.delete(path);
508
517
  if (retry.delay) await sleep(retry.delay);
509
- await revalidateCache({
518
+ return revalidateCache({
510
519
  routeItem,
511
520
  pathname,
512
521
  search,
513
522
  signal
514
523
  }, retried + 1);
515
- return {
516
- data: null,
517
- error
518
- };
519
- } else {
520
- loaderStateRef.set((prev) => ({
521
- ...prev,
522
- data: null,
523
- loaderError: error
524
- }));
525
- return {
526
- data: null,
527
- error
528
- };
529
- }
524
+ } else return {
525
+ data: null,
526
+ error
527
+ };
530
528
  } finally {
531
529
  loadingPromises.delete(path);
532
530
  }
@@ -537,20 +535,6 @@ var createRevalidateCache = (routerState) => {
537
535
  return revalidateCache;
538
536
  };
539
537
  //#endregion
540
- //#region cell.ts
541
- var Cell = class {
542
- constructor(_value) {
543
- _defineProperty(this, "_value", void 0);
544
- this._value = _value;
545
- }
546
- get value() {
547
- return this._value;
548
- }
549
- set(action) {
550
- this._value = typeof action === "function" ? action(this._value) : action;
551
- }
552
- };
553
- //#endregion
554
538
  //#region utils/createRouterInstance.ts
555
539
  var createRouterInstance = () => {
556
540
  const routerState = {
@@ -559,14 +543,14 @@ var createRouterInstance = () => {
559
543
  location: {}
560
544
  }),
561
545
  pendingState: create(void 0),
562
- currentLoaderState: create(emptyLoaderState),
563
546
  scrollMapState: create({}),
564
547
  contextState: create({}),
565
548
  blockedRouteState: create({
566
549
  from: "",
567
550
  to: ""
568
551
  }),
569
- loaderStateRef: new Cell(emptyLoaderState),
552
+ isOptimisticLoading: create(false),
553
+ loaderState: create(emptyLoaderState),
570
554
  loaderMap: /* @__PURE__ */ new Map(),
571
555
  loadingPromises: /* @__PURE__ */ new Map()
572
556
  };
@@ -606,6 +590,8 @@ var createRouterInstance = () => {
606
590
  useScrollMap: () => useGlobalState(routerState.scrollMapState),
607
591
  usePendingState: () => useGlobalState(routerState.pendingState),
608
592
  useContextState: () => useGlobalState(routerState.contextState),
593
+ useOptimisticLoading: () => useGlobalState(routerState.isOptimisticLoading),
594
+ useLoaderState: () => useGlobalState(routerState.loaderState)[0],
609
595
  useParams: () => getParamsObject(),
610
596
  useNavigate: () => {
611
597
  const { blockedRouteState } = routerState;
@@ -681,6 +667,9 @@ var useNavigation = () => {
681
667
  }, [navigate]);
682
668
  };
683
669
  //#endregion
670
+ //#region hooks/useLoaderState.ts
671
+ var useLoaderState = router.hooks.useLoaderState;
672
+ //#endregion
684
673
  //#region hooks/useApplyCustomAnimation.ts
685
674
  var useApplyCustomAnimation = (animationDuration) => {
686
675
  useEffect(() => {
@@ -758,12 +747,13 @@ var EmptyBoundary = ({ children }) => children;
758
747
  var IS_MOBILE = isMobile();
759
748
  var MOBILE_CACHE_SIZE = 60;
760
749
  var DESKTOP_CACHE_SIZE = 150;
761
- var Router = ({ routes, defaultBeforeLoad, defaultAfterLoad, animationDuration, defaultLoaderFallback, defaultErrorElement, defaultRetry, defaultStaleTime, context: initialContext, isAnimated = false, spinner = true, defaultPreserveScroll = true, showFallbackOnAnimation = false, maxCacheSize = IS_MOBILE ? MOBILE_CACHE_SIZE : DESKTOP_CACHE_SIZE, defaultPrefetch = IS_MOBILE ? "viewport" : "hover", defaultHoverPrefetchDelay = 150, errorBoundary: ErrorBoundary = EmptyBoundary }) => {
762
- const { useRouteItemData, usePendingState } = router.hooks;
750
+ var Router = ({ routes, defaultBeforeLoad, defaultAfterLoad, animationDuration, defaultLoaderFallback, defaultErrorElement, defaultRetry, defaultStaleTime, context: initialContext, isAnimated = false, optimisticSpinner = true, defaultPreserveScroll = true, defaultMinLoaderDuration = 0, maxCacheSize = IS_MOBILE ? MOBILE_CACHE_SIZE : DESKTOP_CACHE_SIZE, defaultPrefetch = IS_MOBILE ? "viewport" : "hover", defaultHoverPrefetchDelay = 150, errorBoundary: ErrorBoundary = EmptyBoundary }) => {
751
+ const { useRouteItemData, usePendingState, useOptimisticLoading } = router.hooks;
763
752
  const [routeItemData] = useRouteItemData();
764
- const [pendingState] = usePendingState();
765
- const loaderState = router.state.loaderStateRef.value;
766
- const isLoading = Boolean(pendingState);
753
+ const [pendingRouteData] = usePendingState();
754
+ const [isOptimisticLoading] = useOptimisticLoading();
755
+ const loaderState = useLoaderState();
756
+ const isLoading = Boolean(pendingRouteData);
767
757
  useNavigation();
768
758
  useSetRouterConfig({
769
759
  routes,
@@ -775,6 +765,7 @@ var Router = ({ routes, defaultBeforeLoad, defaultAfterLoad, animationDuration,
775
765
  defaultRetry,
776
766
  defaultStaleTime,
777
767
  defaultPreserveScroll,
768
+ defaultMinLoaderDuration,
778
769
  maxCacheSize
779
770
  });
780
771
  useApplyCustomAnimation(animationDuration);
@@ -782,15 +773,12 @@ var Router = ({ routes, defaultBeforeLoad, defaultAfterLoad, animationDuration,
782
773
  usePreserveScroll(routeItemData);
783
774
  const { routeItem, location } = routeItemData;
784
775
  const showErrorElement = !isLoading && Boolean(loaderState.loaderError || loaderState.beforeLoadError);
785
- const showSpinner = spinner && isAnimated && isLoading;
786
- const loadingContent = !showErrorElement && isLoading;
787
- if ((showFallbackOnAnimation || !isAnimated) && loadingContent) return renderElement(pendingState?.routeItem?.loaderFallback || defaultLoaderFallback);
788
- if (!showFallbackOnAnimation && isAnimated && loadingContent) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
776
+ if (!showErrorElement && isLoading) return renderElement(pendingRouteData?.routeItem?.loaderFallback || defaultLoaderFallback);
789
777
  if (!routeItem) return null;
790
- 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, {})] });
778
+ if (showErrorElement) return renderElement(routeItem.errorElement || defaultErrorElement);
791
779
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
792
780
  style: { viewTransitionName: "page" },
793
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBoundary, { children: renderElement(routeItem.element) }, location.pathname), showSpinner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
781
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBoundary, { children: renderElement(routeItem.element) }, location.pathname), optimisticSpinner && isOptimisticLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
794
782
  });
795
783
  };
796
784
  //#endregion
@@ -932,9 +920,6 @@ var Form = ({ children, action, onSuccess, onError, autoReset = true }) => {
932
920
  //#region hooks/useParams.ts
933
921
  var useParams = router.hooks.useParams;
934
922
  //#endregion
935
- //#region hooks/useLoaderState.ts
936
- var useLoaderState = () => router.state.loaderStateRef.value;
937
- //#endregion
938
923
  //#region hooks/useInvalidate.ts
939
924
  var useInvalidate = () => router.runtime.invalidate;
940
925
  //#endregion
@@ -1,2 +1,2 @@
1
1
  import { type InvalidateOptions, InvalidateResult, RevalidateCache, RouterState } from '../types';
2
- export declare const createInvalidate: ({ routeItemDataState, loaderStateRef, loaderMap, currentLoaderState, contextState }: RouterState, revalidateCache: RevalidateCache) => (pathList?: string | string[], options?: InvalidateOptions) => Promise<InvalidateResult[]>;
2
+ export declare const createInvalidate: ({ routeItemDataState, loaderState, loaderMap, contextState }: RouterState, revalidateCache: RevalidateCache) => (pathList?: string | string[], options?: InvalidateOptions) => Promise<InvalidateResult[]>;
package/dist/types.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { ComponentType, type CSSProperties, Dispatch, MouseEvent, ReactElement, ReactNode, Ref, SetStateAction } from 'react';
2
2
  import { Store, useGlobalState } from './create';
3
- import { Cell } from './cell';
4
3
  export declare const LAZY_MARKER: unique symbol;
5
4
  export type LazyComponent = {
6
5
  readonly [LAZY_MARKER]: true;
@@ -33,6 +32,7 @@ export type ClientRouteItem = {
33
32
  optimistic?: boolean;
34
33
  pollingInterval?: number;
35
34
  retry?: Retry;
35
+ minLoaderDuration?: number;
36
36
  preserveScroll?: boolean;
37
37
  beforeLoad?: BeforeLoad;
38
38
  afterLoad?: (arg: {
@@ -85,15 +85,15 @@ export type RouterProps = {
85
85
  routes: RouteItem[];
86
86
  isAnimated?: boolean;
87
87
  animationDuration?: number;
88
- spinner?: boolean;
88
+ optimisticSpinner?: boolean;
89
89
  defaultPreserveScroll?: boolean;
90
90
  defaultRetry?: Retry;
91
91
  defaultStaleTime?: number;
92
92
  defaultLoaderFallback?: RenderElement;
93
93
  defaultErrorElement?: RenderElement;
94
- showFallbackOnAnimation?: boolean;
95
94
  defaultPrefetch?: 'hover' | 'render' | 'viewport' | 'none';
96
95
  defaultHoverPrefetchDelay?: number;
96
+ defaultMinLoaderDuration?: number;
97
97
  maxCacheSize?: number;
98
98
  errorBoundary?: ComponentType<{
99
99
  children: ReactNode;
@@ -117,14 +117,14 @@ export type LoadingPromise = Promise<{
117
117
  export type RouterState = {
118
118
  routeItemDataState: Store<RouteItemData>;
119
119
  pendingState: Store<RouteItemData | undefined>;
120
- currentLoaderState: Store<LoaderState>;
121
120
  scrollMapState: Store<Record<string, number>>;
122
121
  contextState: Store<Record<string, unknown>>;
123
122
  blockedRouteState: Store<{
124
123
  from: string;
125
124
  to: string;
126
125
  }>;
127
- loaderStateRef: Cell<LoaderState>;
126
+ isOptimisticLoading: Store<boolean>;
127
+ loaderState: Store<LoaderState>;
128
128
  loaderMap: Map<string, LoaderStateItem>;
129
129
  loadingPromises: Map<string, LoadingPromise>;
130
130
  };
@@ -144,6 +144,8 @@ export type RouterType = {
144
144
  useScrollMap: () => ReturnType<typeof useGlobalState<Record<string, number>>>;
145
145
  usePendingState: () => ReturnType<typeof useGlobalState<RouteItemData | undefined>>;
146
146
  useContextState: () => ReturnType<typeof useGlobalState<Record<string, unknown>>>;
147
+ useOptimisticLoading: () => ReturnType<typeof useGlobalState<boolean>>;
148
+ useLoaderState: <T>() => LoaderState<T>;
147
149
  useParams: <T>() => T;
148
150
  useNavigate: () => (arg: Location | string | -1) => Promise<void>;
149
151
  useGetAction: (actionKey: string) => {
@@ -1,3 +1 @@
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
+ export declare const commitNavigation: (callback: () => void) => void;
@@ -1,2 +1,2 @@
1
1
  import { Location, RouteItem, RouterState } from '../types';
2
- export declare const createCommitState: ({ routeItemDataState, pendingState }: RouterState) => (nextLocation: Location, routeItem: RouteItem | undefined) => void;
2
+ export declare const createCommitState: ({ routeItemDataState, pendingState, isOptimisticLoading }: RouterState) => (nextLocation: Location, routeItem: RouteItem | undefined) => void;
@@ -1,8 +1,2 @@
1
- import { RevalidateCacheArgs, RouterState } from '../types';
2
- export declare const createRevalidateCache: (routerState: RouterState) => ({ routeItem, pathname, search, signal }: RevalidateCacheArgs, retried?: number) => Promise<{
3
- data: unknown;
4
- error: null;
5
- } | {
6
- data: null;
7
- error: unknown;
8
- } | undefined>;
1
+ import { LoadingPromise, RevalidateCacheArgs, RouterState } from '../types';
2
+ export declare const createRevalidateCache: (routerState: RouterState) => ({ routeItem, pathname, search, signal }: RevalidateCacheArgs, retried?: number) => LoadingPromise;
@@ -4,3 +4,4 @@ export declare const getParamsObject: (nextItem?: RouteItem, nextPathname?: stri
4
4
  export declare const parseWindowLocation: (location: typeof window.location) => Location;
5
5
  export declare const comparePaths: (route: RouteItem, pathname: string) => boolean;
6
6
  export declare const isMobile: () => boolean;
7
+ export declare const sleep: (ms: number) => Promise<unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {
package/dist/cell.d.ts DELETED
@@ -1,6 +0,0 @@
1
- export declare class Cell<T> {
2
- private _value;
3
- constructor(_value: T);
4
- get value(): T;
5
- set(action: T | ((prev: T) => T)): void;
6
- }