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 +14 -13
- package/dist/components/Router.d.ts +1 -1
- package/dist/config/routerConfig.d.ts +1 -0
- package/dist/index.js +48 -50
- package/dist/types.d.ts +6 -2
- package/dist/utils/commitNavigation.d.ts +1 -3
- package/dist/utils/commitState.d.ts +1 -1
- package/dist/utils/utils.d.ts +1 -0
- package/package.json +1 -1
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
|
-
| `
|
|
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
|
-
| `
|
|
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
|
-
* **
|
|
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,
|
|
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
|
|
107
|
-
|
|
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(
|
|
111
|
+
document.startViewTransition(callback);
|
|
111
112
|
} catch {
|
|
112
|
-
|
|
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")
|
|
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
|
|
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 =
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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,
|
|
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:
|
|
304
|
-
search:
|
|
304
|
+
pathname: nextLocation.pathname,
|
|
305
|
+
search: nextLocation.search,
|
|
305
306
|
signal
|
|
306
307
|
}), routeItem.pollingInterval);
|
|
307
308
|
};
|
|
308
|
-
const
|
|
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:
|
|
315
|
-
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,
|
|
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
|
|
389
|
-
for (const [key] of loaderMap) if (comparePaths(routeItem, key))
|
|
390
|
-
|
|
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,
|
|
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 [
|
|
762
|
+
const [pendingRouteData] = usePendingState();
|
|
763
|
+
const [isOptimisticLoading] = useOptimisticLoading();
|
|
764
764
|
const loaderState = router.state.loaderStateRef.value;
|
|
765
|
-
const isLoading = Boolean(
|
|
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
|
-
|
|
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
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/utils/utils.d.ts
CHANGED
|
@@ -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>;
|