clear-react-router 1.6.2 → 1.6.4
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 +119 -25
- package/dist/components/Router.d.ts +1 -1
- package/dist/components/RouterProvider.d.ts +2 -2
- package/dist/config/routerConfig.d.ts +1 -1
- package/dist/context/RouterProviderContext.d.ts +1 -0
- package/dist/hooks/useHandleNavigation.d.ts +5 -3
- package/dist/hooks/useInvalidate.d.ts +1 -0
- package/dist/hooks/useLoader.d.ts +3 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +111 -62
- package/dist/provider/Provider.d.ts +1 -1
- package/dist/types/global.d.ts +12 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,21 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight, type-safe routing library for React applications with nested routes, data loading, navigation blocking, and prefetching.
|
|
4
4
|
|
|
5
|
+
## Why Clear Router?
|
|
6
|
+
|
|
7
|
+
Most React routers focus on flexibility and ecosystem integrations.
|
|
8
|
+
Clear Router focuses on predictable navigation with a small, explicit API.
|
|
9
|
+
|
|
10
|
+
It provides first-class support for:
|
|
11
|
+
|
|
12
|
+
- Predictable routing
|
|
13
|
+
- Built-in data loading
|
|
14
|
+
- Small API
|
|
15
|
+
|
|
5
16
|
## Features
|
|
6
17
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
18
|
+
- **Nested Routes** - Organize your UI with nested layouts and routes
|
|
19
|
+
- **Data Loading** - Built-in loaders with caching and stale-while-revalidate strategy
|
|
20
|
+
- **Navigation Blocking** - Prevent accidental navigation with `useBlocker`
|
|
21
|
+
- **Smooth Animations** - Page transitions with fade effect (customizable duration)
|
|
22
|
+
- **Static Layout** — Keep navbar, footer, and other elements outside the router to avoid unnecessary re-renders
|
|
23
|
+
- **Programmatic Redirects** - Redirect from beforeLoad hook
|
|
24
|
+
- **Cache invalidation** - Manual route invalidation
|
|
25
|
+
- **Prefetching** - Preload data on hover for instant navigation
|
|
26
|
+
- **Lazy Loading** - Code-split your routes with dynamic imports for optimal performance
|
|
27
|
+
- **Scroll Restoration** — Automatically saves and restores scroll position when navigating back to a page (preserves user's scroll position)
|
|
28
|
+
- **Typed Query Param** — Type-safe reading and writing of URL query parameters with built-in parsers for strings, numbers, booleans, arrays, and Zod schemas
|
|
29
|
+
- **Flexible API** - Use components or hooks as you prefer
|
|
30
|
+
- **Browser History** - Full support for browser back/forward buttons
|
|
31
|
+
- **Context-aware** - Pass and update context through routes
|
|
20
32
|
|
|
21
33
|
## API
|
|
22
34
|
|
|
@@ -28,8 +40,8 @@ Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic pa
|
|
|
28
40
|
|----------|------|-------------|
|
|
29
41
|
| `path` | `string` | Route path, e.g., `/user/:userId` |
|
|
30
42
|
| `element` | `ReactElement \| () => ReactElement \| LazyComponent` | Component to render |
|
|
31
|
-
| `loader` | `({ params, context, setContext }) => Promise<unknown>` | Fetch data using route params and context. Can update context via `setContext` |
|
|
32
43
|
| `beforeLoad` | `({ params, context, redirect, setContext }) => Promise<unknown> \| undefined \| void` | Auth checks and redirects. Can update context via `setContext`. `redirect` is provided by the router |
|
|
44
|
+
| `loader` | `({ params, context, setContext }) => Promise<unknown>` | Fetch data using route params and context. Can update context via `setContext` |
|
|
33
45
|
| `afterLoad` | `({ params, context, setContext }) => Promise<void>` | Analytics, side effects after data is loaded. Can update context via `setContext` |
|
|
34
46
|
| `fallback` | `ReactElement \| () => ReactElement` | Loading fallback (for lazy loading) |
|
|
35
47
|
| `loaderFallback` | `ReactElement \| () => ReactElement` | Loading fallback (for loader) |
|
|
@@ -43,7 +55,7 @@ The root component that provides routing context to the application. Place stati
|
|
|
43
55
|
|
|
44
56
|
| Prop | Type | Default | Description |
|
|
45
57
|
|------|------|---------|-------------|
|
|
46
|
-
| `
|
|
58
|
+
| `routes` | `RouteItem[]` | required | Array of route configurations |
|
|
47
59
|
| `context` | `object` | `{}` | Initial context (user, theme, etc.) |
|
|
48
60
|
| `children` | `ReactNode` | required | App content (must include `<Router />`) |
|
|
49
61
|
|
|
@@ -71,12 +83,13 @@ Renders the current route's component. Must be placed inside `<RouterProvider>`.
|
|
|
71
83
|
| `animationDuration` | `number` | `optional` | Animation duration in milliseconds (browser default is used if not set) |
|
|
72
84
|
| `spinner` | `boolean \| undefined` | `true` | Show a small spinner in the corner while loading data (only when `isAnimated` is enabled) |
|
|
73
85
|
| `preserveScroll` | `boolean \| undefined` | `true` | Save and restore scroll position when navigating between pages |
|
|
74
|
-
| `
|
|
86
|
+
| `showFallbackOnAnimation` | `boolean \| undefined` | `false` | Show `loaderFallback` even when `isAnimated` is `true` (instead of spinner) |
|
|
75
87
|
| `prefetch` | `'hover' \| 'render' \| 'viewport' \| 'none'` | `'hover'` | Default prefetch strategy for all `<Link>` components |
|
|
76
88
|
| `hoverPrefetchDelay` | `number` | `150` | Delay in milliseconds before prefetching on hover (only for `'hover'` strategy) |
|
|
89
|
+
| `errorBoundary` | `ComponentType<{ children: ReactNode }>` | `undefined` | Custom error boundary component for catching render errors in route components |
|
|
77
90
|
|
|
78
91
|
```
|
|
79
|
-
<RouterProvider
|
|
92
|
+
<RouterProvider routes={routes}>
|
|
80
93
|
<Navbar />
|
|
81
94
|
<Router spinner={false} isAnimated /> {/* disable the spinner */}
|
|
82
95
|
</RouterProvider>
|
|
@@ -110,7 +123,7 @@ Component for client-side navigation with prefetch support.
|
|
|
110
123
|
import { RouterProvider, Router, Link } from 'clear-react-router';
|
|
111
124
|
|
|
112
125
|
// Global prefetch: hover with 100ms delay
|
|
113
|
-
<RouterProvider
|
|
126
|
+
<RouterProvider routes={routes} prefetch="hover" hoverPrefetchDelay={100}>
|
|
114
127
|
<Router />
|
|
115
128
|
</RouterProvider>
|
|
116
129
|
|
|
@@ -204,6 +217,23 @@ const routes = createRouter([
|
|
|
204
217
|
]);
|
|
205
218
|
```
|
|
206
219
|
|
|
220
|
+
### Error Boundaries
|
|
221
|
+
|
|
222
|
+
You can provide a custom error boundary to catch rendering errors in route components. This is useful for preventing the entire app from crashing when a specific route fails to render.
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
import { RouterProvider, Router } from 'clear-react-router';
|
|
226
|
+
import { routes } from './routes';
|
|
227
|
+
import { ErrorBoundary } from './components/ErrorBoundary';
|
|
228
|
+
|
|
229
|
+
const App = () => (
|
|
230
|
+
<RouterProvider routes={routes}>
|
|
231
|
+
<Router errorBoundary={ErrorBoundary} />
|
|
232
|
+
</RouterProvider>
|
|
233
|
+
);
|
|
234
|
+
```
|
|
235
|
+
**Note:** The `errorBoundary` prop only catches render-time errors in route components. It does not catch errors in `loader` or `beforeLoad` — those are handled by the router's `errorElement` mechanism.
|
|
236
|
+
|
|
207
237
|
## Hooks
|
|
208
238
|
|
|
209
239
|
### `useNavigate()`
|
|
@@ -245,21 +275,21 @@ Returns current location `{ pathname, search, state }`.
|
|
|
245
275
|
const { pathname, search, state } = useLocation();
|
|
246
276
|
```
|
|
247
277
|
|
|
248
|
-
### `useLoaderState()`
|
|
278
|
+
### `useLoaderState<T>()`
|
|
249
279
|
|
|
250
280
|
Returns the cached data loaded by the current route's `loader`, along with any errors from `loader` or `beforeLoad`. Data is automatically cached and reused when navigating back to the same route.
|
|
251
281
|
|
|
252
282
|
**Returns:**
|
|
253
283
|
|
|
254
284
|
| Property | Type | Description |
|
|
255
|
-
|
|
256
|
-
| `data` | `
|
|
285
|
+
|----------|:----:|-------------|
|
|
286
|
+
| `data` | `T` | The data returned from the route's `loader` |
|
|
257
287
|
| `loaderError` | `Error \| null` | Error from the `loader` (if any) |
|
|
258
288
|
| `beforeLoadError` | `Error \| null` | Error from the `beforeLoad` hook (if any) |
|
|
259
289
|
|
|
260
290
|
```
|
|
261
|
-
|
|
262
|
-
const { data, loaderError, beforeLoadError } = useLoaderState();
|
|
291
|
+
const UserProfile = () => {
|
|
292
|
+
const { data, loaderError, beforeLoadError } = useLoaderState<User>();
|
|
263
293
|
```
|
|
264
294
|
|
|
265
295
|
### Caching behavior:
|
|
@@ -273,6 +303,71 @@ function UserProfile() {
|
|
|
273
303
|
}
|
|
274
304
|
```
|
|
275
305
|
|
|
306
|
+
### `useInvalidate()`
|
|
307
|
+
|
|
308
|
+
Returns a function that marks route data as stale and re-runs the route lifecycle.
|
|
309
|
+
|
|
310
|
+
Calling `invalidate()` clears the cached loader result for a route and executes both `beforeLoad` and `loader` again. This is useful after mutations or any operation that changes data used by the route.
|
|
311
|
+
|
|
312
|
+
#### Current route
|
|
313
|
+
|
|
314
|
+
Invalidate the currently active route:
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
const invalidate = useInvalidate();
|
|
318
|
+
|
|
319
|
+
await invalidate();
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### Specific route
|
|
323
|
+
|
|
324
|
+
You can also invalidate any registered route by passing its pathname:
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
const invalidate = useInvalidate();
|
|
328
|
+
|
|
329
|
+
await invalidate('/posts');
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The route does not need to be currently active. Its cache will be marked as stale, and the next time it is visited, `beforeLoad` and `loader` will run again.
|
|
333
|
+
|
|
334
|
+
#### Why use it?
|
|
335
|
+
|
|
336
|
+
A common use case is refreshing route data after a mutation.
|
|
337
|
+
|
|
338
|
+
For example, after deleting a post while viewing `/posts/42`, you may want the posts list to be reloaded the next time the user navigates to `/posts`:
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
const invalidate = useInvalidate();
|
|
342
|
+
|
|
343
|
+
await deletePost(id);
|
|
344
|
+
await invalidate('/posts');
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Likewise, after updating the current page, you can immediately refresh its data:
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
const invalidate = useInvalidate();
|
|
351
|
+
|
|
352
|
+
await updateProfile(data);
|
|
353
|
+
await invalidate();
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
#### Notes
|
|
357
|
+
|
|
358
|
+
* `invalidate()` re-executes both `beforeLoad` and `loader` for the invalidated route.
|
|
359
|
+
* Cached data is discarded before the new loader starts.
|
|
360
|
+
* When used as an event handler, wrap the call in an arrow function:
|
|
361
|
+
|
|
362
|
+
```tsx
|
|
363
|
+
<button onClick={() => invalidate()}>
|
|
364
|
+
Refresh
|
|
365
|
+
</button>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Passing `invalidate` directly (`onClick={invalidate}`) is not supported because React passes a `MouseEvent` object to event handlers.
|
|
369
|
+
|
|
370
|
+
|
|
276
371
|
### `useBlocker(callback)`
|
|
277
372
|
|
|
278
373
|
Blocks navigation when callback returns `true`.
|
|
@@ -370,7 +465,7 @@ type Adapter<T> = {
|
|
|
370
465
|
| Element | Type | Description |
|
|
371
466
|
|---------|------|-------------|
|
|
372
467
|
| `value` | `T` | The parsed value from the query parameter |
|
|
373
|
-
| `setValue` | `(arg: T
|
|
468
|
+
| `setValue` | `(arg: T \| null) => void` | Function to update the query parameter. Null is passed to remove the parameter. |
|
|
374
469
|
|
|
375
470
|
### Built-in Adapters
|
|
376
471
|
|
|
@@ -489,7 +584,6 @@ function ProductFilter() {
|
|
|
489
584
|
|
|
490
585
|
- **Array support** — `getSearchParams` returns `string[]` when multiple values exist for the same key
|
|
491
586
|
- **Functional updates** — Update parameters based on previous state without losing other params
|
|
492
|
-
- **Type-safe** — Proper TypeScript support with overloads
|
|
493
587
|
- **Stable reference** — `setSearchParams` reference is stable and safe to use in `useEffect`
|
|
494
588
|
|
|
495
589
|
> **Note:** `getSearchParams` returns `string` for single values, `string[]` for multiple values, and `''` if the key is not found.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { RouterProps } from '../types/global';
|
|
2
|
-
export declare const Router: ({ isAnimated, animationDuration, spinner, preserveScroll,
|
|
2
|
+
export declare const Router: ({ isAnimated, animationDuration, spinner, preserveScroll, showFallbackOnAnimation, prefetch, hoverPrefetchDelay, errorBoundary: ErrorBoundary, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -2,8 +2,8 @@ import { ReactNode } from 'react';
|
|
|
2
2
|
import { RouteItem } from '../types/global';
|
|
3
3
|
type RouteProviderProps = {
|
|
4
4
|
children: ReactNode;
|
|
5
|
-
|
|
5
|
+
routes: RouteItem[];
|
|
6
6
|
context?: Record<string, unknown>;
|
|
7
7
|
};
|
|
8
|
-
export declare const RouterProvider: ({ children,
|
|
8
|
+
export declare const RouterProvider: ({ children, routes, context: initialContext }: RouteProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
9
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RouterProps } from '../types/global';
|
|
2
2
|
declare class RouterConfig {
|
|
3
3
|
isAnimated: boolean;
|
|
4
|
-
|
|
4
|
+
showFallbackOnAnimation: boolean;
|
|
5
5
|
prefetch: RouterProps['prefetch'];
|
|
6
6
|
hoverPrefetchDelay: number;
|
|
7
7
|
configure(config: Partial<RouterConfig>): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Dispatch, RefObject, type SetStateAction } from 'react';
|
|
2
2
|
import { BlockerState, LoaderState, Location, RevalidateCacheArgs, RouteItem, RouteItemData, UpdateBlockedRouteProps } from '../types/global';
|
|
3
3
|
type UseHandleNavigation = {
|
|
4
|
-
|
|
4
|
+
routes: RouteItem[];
|
|
5
5
|
context: Record<string, unknown>;
|
|
6
6
|
revalidateCache(arg: RevalidateCacheArgs): Promise<unknown>;
|
|
7
7
|
setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
|
|
@@ -10,15 +10,17 @@ type UseHandleNavigation = {
|
|
|
10
10
|
pathname: string;
|
|
11
11
|
}): boolean;
|
|
12
12
|
loaderStateRef: RefObject<LoaderState>;
|
|
13
|
+
clearTimestamp(path: string): void;
|
|
13
14
|
};
|
|
14
|
-
export declare const useHandleNavigation: ({
|
|
15
|
+
export declare const useHandleNavigation: ({ routes, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef, clearTimestamp, }: UseHandleNavigation) => {
|
|
15
16
|
blockerState: BlockerState;
|
|
16
17
|
updateLocation: (nextLocation: Location) => Promise<void>;
|
|
17
18
|
updateBlockedRoute: ({ type, payload }: UpdateBlockedRouteProps) => void;
|
|
18
19
|
routeItemData: RouteItemData;
|
|
19
20
|
restoreScroll: () => void;
|
|
20
|
-
currentLoaderFallback: import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | (() => import("react").ReactElement) | undefined;
|
|
21
|
+
currentLoaderFallback: (import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | (() => import("react").ReactElement)) | undefined;
|
|
21
22
|
isLoading: boolean;
|
|
22
23
|
loaderState: LoaderState;
|
|
24
|
+
invalidate: (pathname?: string) => Promise<void>;
|
|
23
25
|
};
|
|
24
26
|
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useInvalidate: () => (path?: string) => void;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type Dispatch, type SetStateAction } from 'react';
|
|
2
2
|
import type { LoaderState, RevalidateCacheArgs, RouteItem } from '../types/global';
|
|
3
3
|
type UseLoaderParams = {
|
|
4
|
-
|
|
4
|
+
routes: RouteItem[];
|
|
5
5
|
context: Record<string, unknown>;
|
|
6
6
|
setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
|
|
7
7
|
};
|
|
8
|
-
export declare const useLoader: ({
|
|
8
|
+
export declare const useLoader: ({ routes, context, setContext }: UseLoaderParams) => {
|
|
9
9
|
prefetchLoader: (pathname: string) => Promise<void>;
|
|
10
10
|
revalidateCache: ({ routeItem, pathname }: RevalidateCacheArgs) => Promise<unknown>;
|
|
11
11
|
isCacheItemFresh: ({ routeItem, pathname }: {
|
|
@@ -13,5 +13,6 @@ export declare const useLoader: ({ routeList, context, setContext }: UseLoaderPa
|
|
|
13
13
|
pathname: string;
|
|
14
14
|
}) => boolean;
|
|
15
15
|
loaderStateRef: import("react").RefObject<LoaderState>;
|
|
16
|
+
clearTimestamp: (pathname: string) => void;
|
|
16
17
|
};
|
|
17
18
|
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { useNavigate } from './hooks/useNavigate';
|
|
|
5
5
|
export { useParams } from './hooks/useParams';
|
|
6
6
|
export { useLocation } from './hooks/useLocation';
|
|
7
7
|
export { useLoaderState } from './hooks/useLoaderState';
|
|
8
|
+
export { useInvalidate } from './hooks/useInvalidate';
|
|
8
9
|
export { useBlocker } from './hooks/useBlocker';
|
|
9
10
|
export { useBeforeUnload } from './hooks/useBeforeUnload';
|
|
10
11
|
export { useRouterContext } from './hooks/useRouterContext';
|
|
@@ -13,4 +14,4 @@ export { useSearchParams } from './hooks/useSearchParams';
|
|
|
13
14
|
export { useHistoricalTrail } from './hooks/useHistoricalTrail';
|
|
14
15
|
export { adapter } from './utils/adapter';
|
|
15
16
|
export { createRouter } from './utils/utils';
|
|
16
|
-
export type { RouteItem, BlockerState, Location, AdapterType } from './types/global';
|
|
17
|
+
export type { RouteItem, BlockerState, Location, AdapterType, RouterProps } from './types/global';
|
package/dist/index.js
CHANGED
|
@@ -45,14 +45,15 @@ 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, loaderState }) => {
|
|
48
|
+
var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocation, prefetchLoader, blockerState, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState, invalidate }) => {
|
|
49
49
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionsContext.Provider, {
|
|
50
50
|
value: {
|
|
51
51
|
updateLocation,
|
|
52
52
|
updateBlockedRoute,
|
|
53
53
|
prefetchLoader,
|
|
54
54
|
setContext,
|
|
55
|
-
restoreScroll
|
|
55
|
+
restoreScroll,
|
|
56
|
+
invalidate
|
|
56
57
|
},
|
|
57
58
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DataContext.Provider, {
|
|
58
59
|
value: { context },
|
|
@@ -124,7 +125,7 @@ function _defineProperty(e, r, t) {
|
|
|
124
125
|
var RouterConfig = class {
|
|
125
126
|
constructor() {
|
|
126
127
|
_defineProperty(this, "isAnimated", false);
|
|
127
|
-
_defineProperty(this, "
|
|
128
|
+
_defineProperty(this, "showFallbackOnAnimation", false);
|
|
128
129
|
_defineProperty(this, "prefetch", "hover");
|
|
129
130
|
_defineProperty(this, "hoverPrefetchDelay", 150);
|
|
130
131
|
}
|
|
@@ -187,8 +188,8 @@ var comparePaths = (el, pathname) => {
|
|
|
187
188
|
//#endregion
|
|
188
189
|
//#region hooks/useHandleNavigation.ts
|
|
189
190
|
var ALL_LOCATIONS = "*";
|
|
190
|
-
var useHandleNavigation = ({
|
|
191
|
-
const { isAnimated,
|
|
191
|
+
var useHandleNavigation = ({ routes, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef, clearTimestamp }) => {
|
|
192
|
+
const { isAnimated, showFallbackOnAnimation: showFallback } = routerConfig;
|
|
192
193
|
const [isLoading, setIsLoading] = useState(false);
|
|
193
194
|
const [blockedRoute, setBlockedRoute] = useState({
|
|
194
195
|
from: "",
|
|
@@ -203,16 +204,16 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
|
|
|
203
204
|
const [loaderState, setLoaderState] = useState(emptyLoaderState);
|
|
204
205
|
const prevPathname = useRef("");
|
|
205
206
|
const navigationSeq = useRef(0);
|
|
206
|
-
const
|
|
207
|
+
const scrollMapLatest = useLatest(scrollMap);
|
|
207
208
|
const restoreScroll = useCallback(() => {
|
|
208
|
-
if (!prevPathname.current || !
|
|
209
|
+
if (!prevPathname.current || !scrollMapLatest.current[prevPathname.current]) return;
|
|
209
210
|
requestAnimationFrame(() => {
|
|
210
211
|
window.scrollTo({
|
|
211
|
-
top:
|
|
212
|
+
top: scrollMapLatest.current[prevPathname.current],
|
|
212
213
|
behavior: "smooth"
|
|
213
214
|
});
|
|
214
215
|
});
|
|
215
|
-
}, [
|
|
216
|
+
}, [scrollMapLatest]);
|
|
216
217
|
const navigation = useCallback((nextLocation, routeItem) => {
|
|
217
218
|
setRouteItemData({
|
|
218
219
|
routeItem,
|
|
@@ -237,21 +238,19 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
|
|
|
237
238
|
navigation(nextLocation, routeItem);
|
|
238
239
|
}
|
|
239
240
|
}, [navigation, isAnimated]);
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
params: nextItem?.params,
|
|
247
|
-
pathname: nextLocation.pathname
|
|
241
|
+
const invalidate = useCallback(async (pathname = routeItemData.location.pathname) => {
|
|
242
|
+
if (typeof pathname !== "string") return;
|
|
243
|
+
const routeItem = routes.find((el) => comparePaths(el, pathname));
|
|
244
|
+
const resultParams = getParamsObject({
|
|
245
|
+
params: routeItem?.params,
|
|
246
|
+
pathname
|
|
248
247
|
});
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
await
|
|
248
|
+
clearTimestamp(pathname);
|
|
249
|
+
try {
|
|
250
|
+
if (routeItem?.beforeLoad) await routeItem.beforeLoad({
|
|
252
251
|
context,
|
|
253
|
-
redirect,
|
|
254
|
-
params,
|
|
252
|
+
redirect: () => Promise.resolve(),
|
|
253
|
+
params: resultParams,
|
|
255
254
|
setContext
|
|
256
255
|
});
|
|
257
256
|
loaderStateRef.current = {
|
|
@@ -263,7 +262,50 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
|
|
|
263
262
|
...loaderStateRef.current,
|
|
264
263
|
beforeLoadError: error
|
|
265
264
|
};
|
|
266
|
-
|
|
265
|
+
}
|
|
266
|
+
await revalidateCache({
|
|
267
|
+
routeItem,
|
|
268
|
+
pathname
|
|
269
|
+
});
|
|
270
|
+
if (pathname === routeItemData.location.pathname) setLoaderState(loaderStateRef.current);
|
|
271
|
+
}, [
|
|
272
|
+
clearTimestamp,
|
|
273
|
+
context,
|
|
274
|
+
loaderStateRef,
|
|
275
|
+
revalidateCache,
|
|
276
|
+
routeItemData.location.pathname,
|
|
277
|
+
routes,
|
|
278
|
+
setContext
|
|
279
|
+
]);
|
|
280
|
+
const navigationHandler = useCallback(async (nextLocation) => {
|
|
281
|
+
navigationSeq.current = navigationSeq.current + 1;
|
|
282
|
+
const seq = navigationSeq.current;
|
|
283
|
+
loaderStateRef.current = emptyLoaderState;
|
|
284
|
+
const nextItem = routes.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, nextLocation.pathname));
|
|
285
|
+
const params = getParamsObject({
|
|
286
|
+
params: nextItem?.params,
|
|
287
|
+
pathname: nextLocation.pathname
|
|
288
|
+
});
|
|
289
|
+
if (nextItem?.beforeLoad) {
|
|
290
|
+
const redirect = async (location) => await navigationHandler(typeof location === "string" ? { pathname: location } : location);
|
|
291
|
+
try {
|
|
292
|
+
await nextItem.beforeLoad({
|
|
293
|
+
context,
|
|
294
|
+
redirect,
|
|
295
|
+
params,
|
|
296
|
+
setContext
|
|
297
|
+
});
|
|
298
|
+
loaderStateRef.current = {
|
|
299
|
+
...loaderStateRef.current,
|
|
300
|
+
beforeLoadError: null
|
|
301
|
+
};
|
|
302
|
+
} catch (error) {
|
|
303
|
+
loaderStateRef.current = {
|
|
304
|
+
...loaderStateRef.current,
|
|
305
|
+
beforeLoadError: error
|
|
306
|
+
};
|
|
307
|
+
return transitionedNavigation(nextLocation, nextItem);
|
|
308
|
+
}
|
|
267
309
|
}
|
|
268
310
|
if (seq !== navigationSeq.current) return;
|
|
269
311
|
setScrollMap((prevState) => {
|
|
@@ -295,7 +337,7 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
|
|
|
295
337
|
}, [
|
|
296
338
|
context,
|
|
297
339
|
revalidateCache,
|
|
298
|
-
|
|
340
|
+
routes,
|
|
299
341
|
transitionedNavigation,
|
|
300
342
|
setContext,
|
|
301
343
|
isCacheItemFresh,
|
|
@@ -359,19 +401,20 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
|
|
|
359
401
|
restoreScroll,
|
|
360
402
|
currentLoaderFallback,
|
|
361
403
|
isLoading,
|
|
362
|
-
loaderState
|
|
404
|
+
loaderState,
|
|
405
|
+
invalidate
|
|
363
406
|
};
|
|
364
407
|
};
|
|
365
408
|
//#endregion
|
|
366
409
|
//#region hooks/useLoader.ts
|
|
367
|
-
var useLoader = ({
|
|
368
|
-
const timestampMapRef = useRef(
|
|
410
|
+
var useLoader = ({ routes, context, setContext }) => {
|
|
411
|
+
const timestampMapRef = useRef(/* @__PURE__ */ new Map());
|
|
369
412
|
const loaderMapRef = useRef({});
|
|
370
413
|
const loaderStateRef = useRef(emptyLoaderState);
|
|
371
414
|
const loadingPromises = useRef(/* @__PURE__ */ new Map());
|
|
372
415
|
const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
|
|
373
416
|
if (!routeItem) return true;
|
|
374
|
-
const currentCacheTimestamp = timestampMapRef.current
|
|
417
|
+
const currentCacheTimestamp = timestampMapRef.current.get(pathname);
|
|
375
418
|
if (!currentCacheTimestamp) return false;
|
|
376
419
|
if (!routeItem.staleTime) return true;
|
|
377
420
|
return Date.now() - currentCacheTimestamp < routeItem.staleTime;
|
|
@@ -398,20 +441,13 @@ var useLoader = ({ routeList, context, setContext }) => {
|
|
|
398
441
|
context,
|
|
399
442
|
setContext
|
|
400
443
|
});
|
|
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
|
-
};
|
|
444
|
+
timestampMapRef.current.set(pathname, Date.now());
|
|
410
445
|
loaderStateRef.current = {
|
|
411
446
|
...loaderStateRef?.current,
|
|
412
447
|
data: result,
|
|
413
448
|
loaderError: null
|
|
414
449
|
};
|
|
450
|
+
loaderMapRef.current[pathname] = loaderStateRef.current;
|
|
415
451
|
} catch (error) {
|
|
416
452
|
loaderStateRef.current = {
|
|
417
453
|
...loaderStateRef?.current,
|
|
@@ -431,33 +467,37 @@ var useLoader = ({ routeList, context, setContext }) => {
|
|
|
431
467
|
]);
|
|
432
468
|
return {
|
|
433
469
|
prefetchLoader: useCallback(async (pathname) => {
|
|
434
|
-
const item =
|
|
470
|
+
const item = routes.find((el) => comparePaths(el, pathname));
|
|
435
471
|
if (item) await revalidateCache({
|
|
436
472
|
routeItem: item,
|
|
437
473
|
pathname
|
|
438
474
|
});
|
|
439
|
-
}, [revalidateCache,
|
|
475
|
+
}, [revalidateCache, routes]),
|
|
440
476
|
revalidateCache,
|
|
441
477
|
isCacheItemFresh,
|
|
442
|
-
loaderStateRef
|
|
478
|
+
loaderStateRef,
|
|
479
|
+
clearTimestamp: useCallback((pathname) => {
|
|
480
|
+
timestampMapRef.current.delete(pathname);
|
|
481
|
+
}, [])
|
|
443
482
|
};
|
|
444
483
|
};
|
|
445
484
|
//#endregion
|
|
446
485
|
//#region components/RouterProvider.tsx
|
|
447
|
-
var RouterProvider = ({ children,
|
|
486
|
+
var RouterProvider = ({ children, routes, context: initialContext = {} }) => {
|
|
448
487
|
const [context, setContext] = useState(initialContext);
|
|
449
|
-
const { prefetchLoader, revalidateCache, isCacheItemFresh, loaderStateRef } = useLoader({
|
|
450
|
-
|
|
488
|
+
const { prefetchLoader, revalidateCache, isCacheItemFresh, loaderStateRef, clearTimestamp } = useLoader({
|
|
489
|
+
routes,
|
|
451
490
|
context,
|
|
452
491
|
setContext
|
|
453
492
|
});
|
|
454
|
-
const { blockerState, updateLocation, updateBlockedRoute, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState } = useHandleNavigation({
|
|
455
|
-
|
|
493
|
+
const { blockerState, updateLocation, updateBlockedRoute, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState, invalidate } = useHandleNavigation({
|
|
494
|
+
routes,
|
|
456
495
|
context,
|
|
457
496
|
setContext,
|
|
458
497
|
revalidateCache,
|
|
459
498
|
isCacheItemFresh,
|
|
460
|
-
loaderStateRef
|
|
499
|
+
loaderStateRef,
|
|
500
|
+
clearTimestamp
|
|
461
501
|
});
|
|
462
502
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
|
|
463
503
|
...useMemo(() => ({
|
|
@@ -471,7 +511,8 @@ var RouterProvider = ({ children, routeList, context: initialContext = {} }) =>
|
|
|
471
511
|
restoreScroll,
|
|
472
512
|
currentLoaderFallback,
|
|
473
513
|
isLoading,
|
|
474
|
-
loaderState
|
|
514
|
+
loaderState,
|
|
515
|
+
invalidate
|
|
475
516
|
}), [
|
|
476
517
|
blockerState,
|
|
477
518
|
context,
|
|
@@ -482,7 +523,8 @@ var RouterProvider = ({ children, routeList, context: initialContext = {} }) =>
|
|
|
482
523
|
currentLoaderFallback,
|
|
483
524
|
restoreScroll,
|
|
484
525
|
isLoading,
|
|
485
|
-
loaderState
|
|
526
|
+
loaderState,
|
|
527
|
+
invalidate
|
|
486
528
|
]),
|
|
487
529
|
children
|
|
488
530
|
});
|
|
@@ -548,6 +590,11 @@ var usePreserveScroll = (preserveScroll) => {
|
|
|
548
590
|
]);
|
|
549
591
|
};
|
|
550
592
|
//#endregion
|
|
593
|
+
//#region hooks/useSetRouterConfig.ts
|
|
594
|
+
var useSetRouterConfig = (routerProps) => {
|
|
595
|
+
useEffect(() => routerConfig.configure(routerProps), [routerProps]);
|
|
596
|
+
};
|
|
597
|
+
//#endregion
|
|
551
598
|
//#region components/Spinner.tsx
|
|
552
599
|
var Spinner = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cr-spinner" });
|
|
553
600
|
//#endregion
|
|
@@ -557,32 +604,28 @@ var renderElement = (Component) => {
|
|
|
557
604
|
return typeof Component === "function" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, {}) : Component;
|
|
558
605
|
};
|
|
559
606
|
//#endregion
|
|
560
|
-
//#region hooks/useSetRouterConfig.ts
|
|
561
|
-
var useSetRouterConfig = (routerProps) => {
|
|
562
|
-
useEffect(() => routerConfig.configure(routerProps), [routerProps]);
|
|
563
|
-
};
|
|
564
|
-
//#endregion
|
|
565
607
|
//#region components/Router.tsx
|
|
566
|
-
var
|
|
567
|
-
|
|
608
|
+
var EmptyBoundary = ({ children }) => children;
|
|
609
|
+
var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true, showFallbackOnAnimation = false, prefetch = "hover", hoverPrefetchDelay = 150, errorBoundary: ErrorBoundary = EmptyBoundary }) => {
|
|
610
|
+
const { routeItemData: { routeItem, location }, loaderState, currentLoaderFallback, isLoading } = useNavigationState();
|
|
568
611
|
usePreserveScroll(preserveScroll);
|
|
569
612
|
useApplyCustomAnimation(animationDuration);
|
|
570
613
|
useSetRouterConfig({
|
|
571
614
|
isAnimated,
|
|
572
|
-
|
|
615
|
+
showFallbackOnAnimation,
|
|
573
616
|
prefetch,
|
|
574
617
|
hoverPrefetchDelay
|
|
575
618
|
});
|
|
576
619
|
const showErrorElement = !isLoading && Boolean(loaderState.loaderError || loaderState.beforeLoadError);
|
|
577
620
|
const showSpinner = spinner && isAnimated && isLoading;
|
|
578
621
|
const loadingContent = !showErrorElement && isLoading;
|
|
579
|
-
if ((
|
|
580
|
-
if (!
|
|
622
|
+
if ((showFallbackOnAnimation || !isAnimated) && loadingContent) return renderElement(currentLoaderFallback);
|
|
623
|
+
if (!showFallbackOnAnimation && isAnimated && loadingContent) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
|
|
581
624
|
if (!routeItem) return null;
|
|
582
625
|
if (showErrorElement) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [renderElement(routeItem.errorElement), showSpinner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})] });
|
|
583
626
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
584
627
|
style: { viewTransitionName: "page" },
|
|
585
|
-
children: [renderElement(routeItem.element)
|
|
628
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBoundary, { children: renderElement(routeItem.element) }, location.pathname), showSpinner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
|
|
586
629
|
});
|
|
587
630
|
};
|
|
588
631
|
//#endregion
|
|
@@ -668,11 +711,11 @@ var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }) => {
|
|
|
668
711
|
//#endregion
|
|
669
712
|
//#region hooks/useParams.ts
|
|
670
713
|
var useParams = () => {
|
|
671
|
-
const { routeItemData: { routeItem } } = useNavigationState();
|
|
714
|
+
const { routeItemData: { routeItem, location: { pathname } } } = useNavigationState();
|
|
672
715
|
if (!routeItem) return void 0;
|
|
673
716
|
return getParamsObject({
|
|
674
717
|
params: routeItem?.params,
|
|
675
|
-
pathname
|
|
718
|
+
pathname
|
|
676
719
|
});
|
|
677
720
|
};
|
|
678
721
|
//#endregion
|
|
@@ -682,6 +725,12 @@ var useLoaderState = () => {
|
|
|
682
725
|
return loaderState;
|
|
683
726
|
};
|
|
684
727
|
//#endregion
|
|
728
|
+
//#region hooks/useInvalidate.ts
|
|
729
|
+
var useInvalidate = () => {
|
|
730
|
+
const { invalidate } = useRouterActions();
|
|
731
|
+
return invalidate;
|
|
732
|
+
};
|
|
733
|
+
//#endregion
|
|
685
734
|
//#region hooks/useBlocker.ts
|
|
686
735
|
var useBlocker = (blockerFn) => {
|
|
687
736
|
const { blockerState } = useNavigationState();
|
|
@@ -887,4 +936,4 @@ var adapter = {
|
|
|
887
936
|
})
|
|
888
937
|
};
|
|
889
938
|
//#endregion
|
|
890
|
-
export { Link, Router, RouterProvider, adapter, createRouter, useBeforeUnload, useBlocker, useHistoricalTrail, useLoaderState, useLocation, useNavigate, useParams, useQueryParam, useRouterContext, useSearchParams };
|
|
939
|
+
export { Link, Router, RouterProvider, adapter, createRouter, useBeforeUnload, useBlocker, useHistoricalTrail, useInvalidate, useLoaderState, useLocation, useNavigate, useParams, useQueryParam, useRouterContext, useSearchParams };
|
|
@@ -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, loaderState, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation, prefetchLoader, blockerState, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState, invalidate, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
7
|
export {};
|
package/dist/types/global.d.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import type { ComponentType, Dispatch, ReactElement, SetStateAction } from 'react';
|
|
1
|
+
import type { ComponentType, Dispatch, ReactElement, ReactNode, SetStateAction } from 'react';
|
|
2
2
|
export type LazyComponent = () => Promise<{
|
|
3
3
|
default: ComponentType<unknown>;
|
|
4
4
|
}>;
|
|
5
|
+
type Element = (() => ReactElement) | ReactElement;
|
|
5
6
|
export type ClientRouteItem = {
|
|
6
7
|
path: string;
|
|
7
|
-
element:
|
|
8
|
+
element: Element | LazyComponent;
|
|
8
9
|
loader?(arg: {
|
|
9
10
|
params: Record<string, string>;
|
|
10
11
|
context: Record<string, unknown>;
|
|
11
12
|
setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
|
|
12
13
|
}): Promise<unknown>;
|
|
13
|
-
loaderFallback?:
|
|
14
|
-
errorElement?:
|
|
15
|
-
fallback?:
|
|
14
|
+
loaderFallback?: Element;
|
|
15
|
+
errorElement?: Element;
|
|
16
|
+
fallback?: Element;
|
|
16
17
|
children?: ClientRouteItem[];
|
|
17
18
|
staleTime?: number;
|
|
18
19
|
beforeLoad?: (arg: {
|
|
@@ -28,7 +29,7 @@ export type ClientRouteItem = {
|
|
|
28
29
|
}) => Promise<void>;
|
|
29
30
|
};
|
|
30
31
|
export type RouteItem = ClientRouteItem & {
|
|
31
|
-
element:
|
|
32
|
+
element: Element;
|
|
32
33
|
params?: {
|
|
33
34
|
key: string;
|
|
34
35
|
value: string;
|
|
@@ -67,7 +68,11 @@ export type RouterProps = {
|
|
|
67
68
|
animationDuration?: number;
|
|
68
69
|
spinner?: boolean;
|
|
69
70
|
preserveScroll?: boolean;
|
|
70
|
-
|
|
71
|
+
showFallbackOnAnimation?: boolean;
|
|
71
72
|
prefetch?: 'hover' | 'render' | 'viewport' | 'none';
|
|
72
73
|
hoverPrefetchDelay?: number;
|
|
74
|
+
errorBoundary?: ComponentType<{
|
|
75
|
+
children: ReactNode;
|
|
76
|
+
}>;
|
|
73
77
|
};
|
|
78
|
+
export {};
|