clear-react-router 2.0.4 → 2.0.6

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 |
@@ -639,6 +632,15 @@ This also works for nested dynamic routes:
639
632
  await invalidate('/post/[id]/comment/[id]');
640
633
  ```
641
634
 
635
+ #### Force revalidation
636
+ By default only paths that already exist in the cache are revalidated.
637
+ Pass `{ force: true }` to also revalidate the exact path(s) you passed, even if they were never cached:
638
+
639
+ ```tsx
640
+ await invalidate('/about', { force: true });
641
+ await invalidate(['/about', '/post/10'], { force: true });
642
+ ```
643
+
642
644
  #### Including child routes
643
645
 
644
646
  To revalidate routes together with their cached child routes, pass the `withChildren` option:
@@ -691,7 +693,8 @@ Each object represents a revalidated route, where `path` is the route pathname,
691
693
 
692
694
  #### Notes
693
695
 
694
- * **Only routes that already have cached data are revalidated.**
696
+ * Without `force` option, **only routes that already have cached data** are revalidated.
697
+ * With `force: true`, the exact pathnames you pass are always revalidated (and stored in the cache).
695
698
  * Cached data is cleared before the new loader starts.
696
699
  * When used as an event handler, wrap the call in an arrow function:
697
700
 
@@ -806,8 +809,6 @@ Clear Router supports smooth page transitions using the native View Transitions
806
809
  ## How It Works
807
810
 
808
811
  - **Data loads first** — All `loader` and `beforeLoad` hooks complete before animation starts
809
- - **No `loaderFallback`** — The `loaderFallback` is not shown during animated transitions
810
- - **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
811
812
  - **Native API** — Uses `document.startViewTransition` for smooth, hardware-accelerated animations
812
813
 
813
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;
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,14 +226,15 @@ 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 { loaderStateRef, 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
@@ -279,44 +281,47 @@ 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
292
  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
- }
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
+ 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)]);
318
323
  if (seq !== navigationSeq) return;
319
- polling(routeItem, location);
324
+ polling(routeItem, nextLocation);
320
325
  };
321
326
  const afterLoad = async (routeItem, params) => {
322
327
  const { defaultAfterLoad } = routerConfig;
@@ -338,7 +343,7 @@ var createNavigate = (routerState, revalidateCache) => {
338
343
  prepareNavigation(nextItem, nextLocation);
339
344
  await loader(nextItem, nextLocation, seq);
340
345
  if (seq !== navigationSeq) return;
341
- commitNavigation(nextLocation, nextItem);
346
+ commitNavigation(() => navigationExecutor(nextLocation, nextItem));
342
347
  await afterLoad(nextItem, params);
343
348
  };
344
349
  return navigate;
@@ -385,9 +390,10 @@ var createInvalidate = ({ routeItemDataState, loaderStateRef, loaderMap, current
385
390
  const invalidateItem = async (pathname, options) => {
386
391
  const routeItem = findRoute(pathname);
387
392
  if (!routeItem) return [];
388
- const pathnameArray = [];
389
- for (const [key] of loaderMap) if (comparePaths(routeItem, key)) pathnameArray.push(key);
390
- const currentResults = await Promise.all(pathnameArray.map((pathname) => invalidatePath(routeItem, pathname, options)));
393
+ const pathnameSet = /* @__PURE__ */ new Set();
394
+ for (const [key] of loaderMap) if (comparePaths(routeItem, key)) pathnameSet.add(key);
395
+ if (options?.force) pathnameSet.add(pathname);
396
+ const currentResults = await Promise.all([...pathnameSet].map((pathname) => invalidatePath(routeItem, pathname, options)));
391
397
  if (!options?.withChildren || !routeItem.children?.length) return currentResults;
392
398
  const childResults = await Promise.all(routeItem.children.map((child) => invalidateItem(`${pathname}${child.path}`, options)));
393
399
  return [...currentResults, ...childResults.flat()];
@@ -429,16 +435,8 @@ var getRetry = (routeItem) => {
429
435
  delay: routeRetry ? routeRetry.delay : globalRetry?.delay || 0
430
436
  };
431
437
  };
432
- var sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
433
438
  var createRevalidateCache = (routerState) => {
434
439
  const { loaderStateRef, contextState, loaderMap, loadingPromises } = routerState;
435
- const removeStaleItems = () => {
436
- const deletedItems = [...loaderMap.entries()].filter(([, item]) => {
437
- const staleTime = item.staleTime ?? routerConfig.defaultStaleTime;
438
- return staleTime && staleTime + item.timestamp < Date.now();
439
- });
440
- if (deletedItems.length) deletedItems.forEach((item) => loaderMap.delete(item[0]));
441
- };
442
440
  const evict = () => {
443
441
  if (loaderMap.size <= routerConfig.maxCacheSize) return;
444
442
  const oldestKey = loaderMap.keys().next().value;
@@ -455,7 +453,6 @@ var createRevalidateCache = (routerState) => {
455
453
  const revalidateCache = async ({ routeItem, pathname, search = "", signal }, retried = 0) => {
456
454
  if (!routeItem?.loader) return;
457
455
  const isCacheItemFresh = createIsCacheItemFresh(loaderMap);
458
- removeStaleItems();
459
456
  const path = `${pathname}${search}`;
460
457
  if (loadingPromises.has(path)) {
461
458
  moveItemToLastPosition(path);
@@ -565,6 +562,7 @@ var createRouterInstance = () => {
565
562
  from: "",
566
563
  to: ""
567
564
  }),
565
+ isOptimisticLoading: create(false),
568
566
  loaderStateRef: new Cell(emptyLoaderState),
569
567
  loaderMap: /* @__PURE__ */ new Map(),
570
568
  loadingPromises: /* @__PURE__ */ new Map()
@@ -605,6 +603,7 @@ var createRouterInstance = () => {
605
603
  useScrollMap: () => useGlobalState(routerState.scrollMapState),
606
604
  usePendingState: () => useGlobalState(routerState.pendingState),
607
605
  useContextState: () => useGlobalState(routerState.contextState),
606
+ useOptimisticLoading: () => useGlobalState(routerState.isOptimisticLoading),
608
607
  useParams: () => getParamsObject(),
609
608
  useNavigate: () => {
610
609
  const { blockedRouteState } = routerState;
@@ -757,12 +756,13 @@ var EmptyBoundary = ({ children }) => children;
757
756
  var IS_MOBILE = isMobile();
758
757
  var MOBILE_CACHE_SIZE = 60;
759
758
  var DESKTOP_CACHE_SIZE = 150;
760
- 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 }) => {
761
- const { useRouteItemData, usePendingState } = router.hooks;
759
+ 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 }) => {
760
+ const { useRouteItemData, usePendingState, useOptimisticLoading } = router.hooks;
762
761
  const [routeItemData] = useRouteItemData();
763
- const [pendingState] = usePendingState();
762
+ const [pendingRouteData] = usePendingState();
763
+ const [isOptimisticLoading] = useOptimisticLoading();
764
764
  const loaderState = router.state.loaderStateRef.value;
765
- const isLoading = Boolean(pendingState);
765
+ const isLoading = Boolean(pendingRouteData);
766
766
  useNavigation();
767
767
  useSetRouterConfig({
768
768
  routes,
@@ -774,6 +774,7 @@ var Router = ({ routes, defaultBeforeLoad, defaultAfterLoad, animationDuration,
774
774
  defaultRetry,
775
775
  defaultStaleTime,
776
776
  defaultPreserveScroll,
777
+ defaultMinLoaderDuration,
777
778
  maxCacheSize
778
779
  });
779
780
  useApplyCustomAnimation(animationDuration);
@@ -781,15 +782,12 @@ var Router = ({ routes, defaultBeforeLoad, defaultAfterLoad, animationDuration,
781
782
  usePreserveScroll(routeItemData);
782
783
  const { routeItem, location } = routeItemData;
783
784
  const showErrorElement = !isLoading && Boolean(loaderState.loaderError || loaderState.beforeLoadError);
784
- const showSpinner = spinner && isAnimated && isLoading;
785
- const loadingContent = !showErrorElement && isLoading;
786
- if ((showFallbackOnAnimation || !isAnimated) && loadingContent) return renderElement(pendingState?.routeItem?.loaderFallback || defaultLoaderFallback);
787
- if (!showFallbackOnAnimation && isAnimated && loadingContent) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
785
+ if (!showErrorElement && isLoading) return renderElement(pendingRouteData?.routeItem?.loaderFallback || defaultLoaderFallback);
788
786
  if (!routeItem) return null;
789
- 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, {})] });
787
+ if (showErrorElement) return renderElement(routeItem.errorElement || defaultErrorElement);
790
788
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
791
789
  style: { viewTransitionName: "page" },
792
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBoundary, { children: renderElement(routeItem.element) }, location.pathname), showSpinner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
790
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBoundary, { children: renderElement(routeItem.element) }, location.pathname), optimisticSpinner && isOptimisticLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
793
791
  });
794
792
  };
795
793
  //#endregion
package/dist/types.d.ts CHANGED
@@ -33,6 +33,7 @@ export type ClientRouteItem = {
33
33
  optimistic?: boolean;
34
34
  pollingInterval?: number;
35
35
  retry?: Retry;
36
+ minLoaderDuration?: number;
36
37
  preserveScroll?: boolean;
37
38
  beforeLoad?: BeforeLoad;
38
39
  afterLoad?: (arg: {
@@ -85,15 +86,15 @@ export type RouterProps = {
85
86
  routes: RouteItem[];
86
87
  isAnimated?: boolean;
87
88
  animationDuration?: number;
88
- spinner?: boolean;
89
+ optimisticSpinner?: boolean;
89
90
  defaultPreserveScroll?: boolean;
90
91
  defaultRetry?: Retry;
91
92
  defaultStaleTime?: number;
92
93
  defaultLoaderFallback?: RenderElement;
93
94
  defaultErrorElement?: RenderElement;
94
- showFallbackOnAnimation?: boolean;
95
95
  defaultPrefetch?: 'hover' | 'render' | 'viewport' | 'none';
96
96
  defaultHoverPrefetchDelay?: number;
97
+ defaultMinLoaderDuration?: number;
97
98
  maxCacheSize?: number;
98
99
  errorBoundary?: ComponentType<{
99
100
  children: ReactNode;
@@ -124,6 +125,7 @@ export type RouterState = {
124
125
  from: string;
125
126
  to: string;
126
127
  }>;
128
+ isOptimisticLoading: Store<boolean>;
127
129
  loaderStateRef: Cell<LoaderState>;
128
130
  loaderMap: Map<string, LoaderStateItem>;
129
131
  loadingPromises: Map<string, LoadingPromise>;
@@ -144,6 +146,7 @@ export type RouterType = {
144
146
  useScrollMap: () => ReturnType<typeof useGlobalState<Record<string, number>>>;
145
147
  usePendingState: () => ReturnType<typeof useGlobalState<RouteItemData | undefined>>;
146
148
  useContextState: () => ReturnType<typeof useGlobalState<Record<string, unknown>>>;
149
+ useOptimisticLoading: () => ReturnType<typeof useGlobalState<boolean>>;
147
150
  useParams: <T>() => T;
148
151
  useNavigate: () => (arg: Location | string | -1) => Promise<void>;
149
152
  useGetAction: (actionKey: string) => {
@@ -157,6 +160,7 @@ export type RouterType = {
157
160
  export type InvalidateOptions = {
158
161
  withChildren?: boolean;
159
162
  withBeforeLoad?: boolean;
163
+ force?: boolean;
160
164
  };
161
165
  export type RevalidateCache = (args: RevalidateCacheArgs) => LoadingPromise;
162
166
  export type Options = Partial<{
@@ -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;
@@ -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.4",
3
+ "version": "2.0.6",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {