clear-react-router 1.2.5 → 1.3.1
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 +133 -45
- package/dist/components/Router.d.ts +3 -9
- package/dist/components/RouterProvider.d.ts +11 -0
- package/dist/context/{RouterContext.d.ts → RouterProviderContext.d.ts} +8 -2
- package/dist/context/RouterViewContext.d.ts +1 -0
- package/dist/hooks/useApplyCustomAnimation.d.ts +1 -2
- package/dist/hooks/useNavigate.d.ts +2 -2
- package/dist/hooks/useSearchParams.d.ts +10 -0
- package/dist/hooks/useServiceContext.d.ts +4 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +168 -130
- package/dist/provider/Provider.d.ts +7 -0
- package/dist/provider/ViewProvider.d.ts +7 -0
- package/dist/types/global.d.ts +1 -5
- package/package.json +1 -1
- package/dist/provider/RouterProvider.d.ts +0 -7
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@ A lightweight, type-safe routing library for React applications with nested rout
|
|
|
7
7
|
- 🧩 **Nested Routes** - Organize your UI with nested layouts and routes
|
|
8
8
|
- ⚡ **Data Loading** - Built-in loaders with caching and stale-while-revalidate strategy
|
|
9
9
|
- 🔒 **Navigation Blocking** - Prevent accidental navigation with `useBlocker`
|
|
10
|
-
- ✨ **Smooth Animations** - Page transitions with fade
|
|
10
|
+
- ✨ **Smooth Animations** - Page transitions with fade effect (customizable duration)
|
|
11
|
+
- 🏗️ **Static Layout** — Keep navbar, footer, and other elements outside the router to avoid unnecessary re-renders
|
|
11
12
|
- 🎯 **Type-safe Redirects** - Redirect from loaders and beforeLoad hooks
|
|
12
13
|
- 📦 **Prefetching** - Preload data on hover for instant navigation
|
|
13
14
|
- 🚀 **Lazy Loading** - Code-split your routes with dynamic imports for optimal performance
|
|
@@ -31,28 +32,51 @@ Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic pa
|
|
|
31
32
|
| `fallback` | `ReactElement \| () => ReactElement` | Loading fallback (for lazy loading) |
|
|
32
33
|
| `loaderFallback` | `ReactElement \| () => ReactElement` | Loading fallback (for loader) |
|
|
33
34
|
| `errorElement` | `ReactElement \| () => ReactElement` | Error fallback |
|
|
34
|
-
| `staleTime` | `number` |
|
|
35
|
+
| `staleTime` | `number` | Time in ms before cached data is considered stale and re-fetched in the background. If not provided, data never expires (cached forever) |
|
|
35
36
|
| `children` | `RouteItem[]` | Nested routes |
|
|
36
37
|
|
|
37
|
-
### `
|
|
38
|
+
### `RouterProvider`
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
The root component that provides routing context to the application. Place static UI elements (like navbar or footer) outside `<Router />` to prevent unnecessary re-renders.
|
|
40
41
|
|
|
41
42
|
| Prop | Type | Default | Description |
|
|
42
43
|
|------|------|---------|-------------|
|
|
43
44
|
| `routeList` | `RouteItem[]` | required | Array of route configurations |
|
|
44
|
-
| `context` | `object` | `{}` |
|
|
45
|
+
| `context` | `object` | `{}` | Initial context (user, theme, etc.) |
|
|
45
46
|
| `isAnimated` | `boolean` | `false` | Enable smooth page transitions |
|
|
46
|
-
| `
|
|
47
|
+
| `animationDuration` | `number` | `optional` | Animation duration in milliseconds (browser default is used if not set) |
|
|
48
|
+
| `children` | `ReactNode` | required | App content (must include `<Router />`) |
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
function App() {
|
|
52
|
+
return (
|
|
53
|
+
<RouterProvider routeList={routes} isAnimated animationDuration={800}>
|
|
54
|
+
<Navbar /> {/* Static — won't re-render on route change */}
|
|
55
|
+
<main>
|
|
56
|
+
<Router /> {/* Dynamic — renders current page */}
|
|
57
|
+
</main>
|
|
58
|
+
<Footer /> {/* Static — won't re-render on route change */}
|
|
59
|
+
</RouterProvider>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### `Router`
|
|
47
65
|
|
|
48
|
-
|
|
66
|
+
Renders the current route's component. Must be placed inside `<RouterProvider>`.
|
|
49
67
|
|
|
50
|
-
|
|
|
51
|
-
|
|
52
|
-
| `
|
|
53
|
-
| `name` | `'fade' \| 'slide-left' \| 'slide-right'` | `'fade'` | Type of transition effect |
|
|
68
|
+
| Prop | Type | Default | Description |
|
|
69
|
+
|------|------|---------|-------------|
|
|
70
|
+
| `spinner` | `boolean /| undefined` | `true` | Show a small spinner in the corner while loading data (only when `isAnimated` is enabled) |
|
|
54
71
|
|
|
55
|
-
|
|
72
|
+
```
|
|
73
|
+
<RouterProvider routeList={routes} isAnimated>
|
|
74
|
+
<Navbar />
|
|
75
|
+
<Router spinner={false} /> {/* disable the spinner */}
|
|
76
|
+
</RouterProvider>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> **Note:** When `isAnimated` is enabled, `loaderFallback` is not shown. Instead, a small spinner appears (if `spinner={true}`).
|
|
56
80
|
|
|
57
81
|
### `Link`
|
|
58
82
|
|
|
@@ -68,7 +92,7 @@ Component for client-side navigation with prefetch support.
|
|
|
68
92
|
|
|
69
93
|
Function provided to `beforeLoad` for programmatic redirection.
|
|
70
94
|
|
|
71
|
-
**Type:** `(arg: Location) => Promise<void>`
|
|
95
|
+
**Type:** `(arg: Location | string) => Promise<void>`
|
|
72
96
|
|
|
73
97
|
```
|
|
74
98
|
import type { Location } from 'clear-react-router';
|
|
@@ -78,12 +102,23 @@ const routes = createRouter([
|
|
|
78
102
|
path: '/dashboard',
|
|
79
103
|
element: <Dashboard />,
|
|
80
104
|
beforeLoad: ({ context, redirect }) => {
|
|
81
|
-
if (!context.isAuthorized)
|
|
82
|
-
return redirect({ pathname: '/' });
|
|
83
|
-
}
|
|
105
|
+
if (!context.isAuthorized) return redirect('/');
|
|
84
106
|
},
|
|
85
107
|
},
|
|
86
108
|
]);
|
|
109
|
+
|
|
110
|
+
or
|
|
111
|
+
|
|
112
|
+
const routes = createRouter([
|
|
113
|
+
{
|
|
114
|
+
path: '/dashboard',
|
|
115
|
+
element: <Dashboard />,
|
|
116
|
+
beforeLoad: ({ context, redirect }) => {
|
|
117
|
+
if (!context.isAuthorized) return redirect({ pathname: '/login', state: { from: '/dashboard' } });
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
|
|
87
122
|
```
|
|
88
123
|
|
|
89
124
|
### Usage with Parameters
|
|
@@ -103,11 +138,11 @@ const routes = createRouter([
|
|
|
103
138
|
beforeLoad: async ({ params, context, redirect }) => {
|
|
104
139
|
// Authentication check
|
|
105
140
|
if (!context.isAuthorized) {
|
|
106
|
-
return redirect(
|
|
141
|
+
return redirect('/login');
|
|
107
142
|
}
|
|
108
143
|
// Validate parameter
|
|
109
144
|
if (!params.userId || !isValidUserId(params.userId)) {
|
|
110
|
-
return redirect(
|
|
145
|
+
return redirect('/users');
|
|
111
146
|
}
|
|
112
147
|
},
|
|
113
148
|
afterLoad: ({ params, context }) => {
|
|
@@ -122,11 +157,15 @@ const routes = createRouter([
|
|
|
122
157
|
|
|
123
158
|
### `useNavigate()`
|
|
124
159
|
|
|
125
|
-
Returns function to navigate programmatically
|
|
160
|
+
Returns function to navigate programmatically. Accepts a string (pathname), an object with `pathname`, `search`, and `state`, or `-1` to go back.
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
const navigate = useNavigate();
|
|
126
164
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
165
|
+
navigate('/about'); // string
|
|
166
|
+
navigate({ pathname: '/user/123', state: { from: 'home' } }); // object
|
|
167
|
+
navigate(-1); // go back
|
|
168
|
+
```
|
|
130
169
|
|
|
131
170
|
**Note:** Navigation state can be accessed via `useLocation()`:
|
|
132
171
|
|
|
@@ -157,9 +196,22 @@ const { pathname, search, state } = useLocation();
|
|
|
157
196
|
|
|
158
197
|
### `useLoaderState()`
|
|
159
198
|
|
|
160
|
-
Returns
|
|
199
|
+
Returns the cached data loaded by the current route's `loader`. Data is automatically cached and reused when navigating back to the same route.
|
|
200
|
+
```
|
|
201
|
+
const UserProfile = () => {
|
|
202
|
+
const user = useLoaderState();
|
|
203
|
+
return <div>{user.name}</div>;
|
|
204
|
+
}
|
|
161
205
|
```
|
|
162
|
-
|
|
206
|
+
### Caching behavior:
|
|
207
|
+
- The loader result is cached and reused when navigating back to the same route (e.g., from /user/123 back to /user/456 it will be a new request because different params, but from /user/456 to /user/456 — cache hit).
|
|
208
|
+
- Use staleTime in route config to control how long cache is considered fresh:
|
|
209
|
+
```
|
|
210
|
+
{
|
|
211
|
+
path: '/user/:userId',
|
|
212
|
+
loader: async ({ params }) => fetchUser(params.userId),
|
|
213
|
+
staleTime: 60000, // 1 minute — cache is fresh for 60 seconds
|
|
214
|
+
}
|
|
163
215
|
```
|
|
164
216
|
|
|
165
217
|
### `useBlocker(callback)`
|
|
@@ -214,12 +266,59 @@ useBeforeUnload(text ? onSave : undefined);
|
|
|
214
266
|
|
|
215
267
|
### `useRouterContext()`
|
|
216
268
|
|
|
217
|
-
|
|
269
|
+
Returns the router context object and a function to update it. Useful for accessing or modifying global state (like user authentication, theme, etc.) from anywhere in your app.
|
|
218
270
|
```
|
|
219
271
|
const { setContext, context } = useRouterContext();
|
|
220
272
|
const loginHandler = () => setContext({ ...context, user: { name: 'John' } });
|
|
221
273
|
```
|
|
222
274
|
|
|
275
|
+
### `useSearchParams()`
|
|
276
|
+
|
|
277
|
+
Returns an object for working with URL query parameters. Supports reading and setting both single values and arrays.
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
import { useSearchParams } from 'clear-react-router';
|
|
281
|
+
|
|
282
|
+
function ProductFilter() {
|
|
283
|
+
const { searchParams, getSearchParams, setSearchParams } = useSearchParams();
|
|
284
|
+
|
|
285
|
+
// Get a single value or array
|
|
286
|
+
const brand = getSearchParams('brand'); // 'nike' | ['nike', 'reebok'] | ''
|
|
287
|
+
|
|
288
|
+
// Set a single value
|
|
289
|
+
setSearchParams('brand', 'nike'); // ?brand=nike
|
|
290
|
+
|
|
291
|
+
// Set multiple values (array)
|
|
292
|
+
setSearchParams('brand', ['nike', 'reebok']); // ?brand=nike&brand=reebok
|
|
293
|
+
|
|
294
|
+
// Functional update (preserves other params)
|
|
295
|
+
setSearchParams((prev) => {
|
|
296
|
+
prev.set('page', '2');
|
|
297
|
+
prev.append('color', 'red');
|
|
298
|
+
return prev;
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Direct access to URLSearchParams
|
|
302
|
+
const allParams = searchParams.toString(); // "brand=nike&brand=reebok&page=2"
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
**Returns:**
|
|
306
|
+
|
|
307
|
+
| Property | Type | Description |
|
|
308
|
+
|----------|------|-------------|
|
|
309
|
+
| `searchParams` | `URLSearchParams` | Raw `URLSearchParams` object for low-level access |
|
|
310
|
+
| `getSearchParams` | `(key: string) => string \| string[]` | Returns a single value or an array if multiple values exist for the key |
|
|
311
|
+
| `setSearchParams` | `(param: string, value: string \| string[]) => void` `or` `(updater: (prev: URLSearchParams) => URLSearchParams) => void` | Update query parameters. Supports single values, arrays, or functional updates |
|
|
312
|
+
|
|
313
|
+
**Key features:**
|
|
314
|
+
|
|
315
|
+
- ✅ **Array support** — `getSearchParams` returns `string[]` when multiple values exist for the same key
|
|
316
|
+
- ✅ **Functional updates** — Update parameters based on previous state without losing other params
|
|
317
|
+
- ✅ **Type-safe** — Proper TypeScript support with overloads
|
|
318
|
+
- ✅ **Stable reference** — `setSearchParams` reference is stable and safe to use in `useEffect`
|
|
319
|
+
|
|
320
|
+
> **Note:** `getSearchParams` returns `string` for single values, `string[]` for multiple values, and `''` if the key is not found.
|
|
321
|
+
|
|
223
322
|
## Lazy Loading
|
|
224
323
|
|
|
225
324
|
Clear Router supports code-splitting out of the box. Simply pass a function that returns a dynamic import:
|
|
@@ -239,35 +338,24 @@ Clear Router supports smooth page transitions using the native View Transitions
|
|
|
239
338
|
```
|
|
240
339
|
import { Router } from 'clear-react-router';
|
|
241
340
|
|
|
242
|
-
// Enable
|
|
243
|
-
<
|
|
341
|
+
// Enable fade animation
|
|
342
|
+
<RouterProvider routeList={routes} isAnimated>
|
|
343
|
+
<Router />
|
|
344
|
+
</RouterProvider>
|
|
244
345
|
|
|
245
|
-
// Custom animation
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
animationOptions={{
|
|
250
|
-
duration: 500, // milliseconds
|
|
251
|
-
name: 'slide-left' // 'fade' | 'slide-left' | 'slide-right'
|
|
252
|
-
}}
|
|
253
|
-
/>
|
|
346
|
+
// Custom animation duration
|
|
347
|
+
<RouterProvider routeList={routes} isAnimated animationDuration={800}>
|
|
348
|
+
<Router />
|
|
349
|
+
</RouterProvider>
|
|
254
350
|
```
|
|
255
351
|
|
|
256
352
|
## How It Works
|
|
257
353
|
|
|
258
354
|
- **Data loads first** — All `loader` and `beforeLoad` hooks complete before animation starts
|
|
259
355
|
- **No `loaderFallback`** — The `loaderFallback` is not shown during animated transitions
|
|
260
|
-
- **Subtle spinner** — A small spinner appears in the top-left corner while data is loading, so users know the app is responsive
|
|
356
|
+
- **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
|
|
261
357
|
- **Native API** — Uses `document.startViewTransition` for smooth, hardware-accelerated animations
|
|
262
358
|
|
|
263
|
-
## Animation Types
|
|
264
|
-
|
|
265
|
-
| Name | Effect |
|
|
266
|
-
|------|--------|
|
|
267
|
-
| `fade` | Cross-fade between pages (default) |
|
|
268
|
-
| `slide-left` | New page slides in from right, old page slides out to left |
|
|
269
|
-
| `slide-right` | New page slides in from left, old page slides out to right |
|
|
270
|
-
|
|
271
359
|
## Browser Support
|
|
272
360
|
|
|
273
361
|
View Transitions API requires modern browsers:
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
context?: Record<string, unknown>;
|
|
5
|
-
isAnimated?: boolean;
|
|
6
|
-
animationOptions?: AnimationOptions;
|
|
7
|
-
};
|
|
8
|
-
export declare const Router: ({ routeList, context: initialContext, isAnimated, animationOptions, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
9
|
-
export {};
|
|
1
|
+
export declare const Router: ({ spinner }: {
|
|
2
|
+
spinner?: boolean;
|
|
3
|
+
}) => import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { RouteItem } from '../types/global';
|
|
3
|
+
type RouteProviderProps = {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
routeList: RouteItem[];
|
|
6
|
+
context?: Record<string, unknown>;
|
|
7
|
+
isAnimated?: boolean;
|
|
8
|
+
animationDuration?: number;
|
|
9
|
+
};
|
|
10
|
+
export declare const RouterProvider: ({ children, routeList, context: initialContext, isAnimated, animationDuration, }: RouteProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { BlockerState, Location, RouteItem, UpdateBlockedRouteProps } from '../types/global';
|
|
2
|
+
export type PropsContextValue = {
|
|
3
|
+
routeList: RouteItem[];
|
|
4
|
+
isAnimated: boolean;
|
|
5
|
+
};
|
|
2
6
|
export type NavigationContextValue = {
|
|
3
7
|
location: Location;
|
|
4
|
-
params: Record<string, string>;
|
|
5
8
|
blockerState: BlockerState;
|
|
9
|
+
isLoading: boolean;
|
|
10
|
+
shouldErrorElementShown: boolean;
|
|
6
11
|
};
|
|
7
12
|
export type ActionsContextValue = {
|
|
8
13
|
updateLocation(route: Location): Promise<void>;
|
|
@@ -14,6 +19,7 @@ export type DataContextValue = {
|
|
|
14
19
|
loaderCache: unknown;
|
|
15
20
|
context: Record<string, unknown>;
|
|
16
21
|
};
|
|
22
|
+
export declare const PropsContext: import("react").Context<PropsContextValue>;
|
|
17
23
|
export declare const ActionsContext: import("react").Context<ActionsContextValue>;
|
|
18
24
|
export declare const DataContext: import("react").Context<DataContextValue>;
|
|
19
25
|
export declare const NavigationContext: import("react").Context<NavigationContextValue>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const RouterViewContext: import("react").Context<Record<string, string>>;
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const useApplyCustomAnimation: (animationOptions: AnimationOptions) => void;
|
|
1
|
+
export declare const useApplyCustomAnimation: (animationDuration?: number) => void;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { Location } from '../types/global
|
|
2
|
-
export declare const useNavigate: () => (arg: Location |
|
|
1
|
+
import type { Location } from '../types/global';
|
|
2
|
+
export declare const useNavigate: () => (arg: Location | string | number) => Promise<void>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type UseSearchParamsReturn = {
|
|
2
|
+
searchParams: URLSearchParams;
|
|
3
|
+
getSearchParams(arg: string): string | string[];
|
|
4
|
+
setSearchParams: {
|
|
5
|
+
(param: string, value: string | string[]): void;
|
|
6
|
+
(param: (prevState: URLSearchParams) => URLSearchParams): void;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export declare const useSearchParams: () => UseSearchParamsReturn;
|
|
10
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export declare const useNavigationState: () => import("../context/
|
|
2
|
-
export declare const useRouterActions: () => import("../context/
|
|
3
|
-
export declare const useRouterData: () => import("../context/
|
|
1
|
+
export declare const useNavigationState: () => import("../context/RouterProviderContext").NavigationContextValue;
|
|
2
|
+
export declare const useRouterActions: () => import("../context/RouterProviderContext").ActionsContextValue;
|
|
3
|
+
export declare const useRouterData: () => import("../context/RouterProviderContext").DataContextValue;
|
|
4
|
+
export declare const usePropsData: () => import("../context/RouterProviderContext").PropsContextValue;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { RouterProvider } from './components/RouterProvider';
|
|
1
2
|
export { Router } from './components/Router';
|
|
2
3
|
export { Link } from './components/Link';
|
|
3
4
|
export { useNavigate } from './hooks/useNavigate';
|
|
@@ -7,5 +8,6 @@ export { useLoaderState } from './hooks/useLoaderState';
|
|
|
7
8
|
export { useBlocker } from './hooks/useBlocker';
|
|
8
9
|
export { useBeforeUnload } from './hooks/useBeforeUnload';
|
|
9
10
|
export { useRouterContext } from './hooks/useRouterContext';
|
|
11
|
+
export { useSearchParams } from './hooks/useSearchParams';
|
|
10
12
|
export { createRouter } from './utils/utils';
|
|
11
13
|
export type { RouteItem, BlockerState, Location } from './types/global';
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import { Suspense, createContext, lazy, useCallback, useContext, useEffect, useM
|
|
|
2
2
|
//#region \0rolldown/runtime.js
|
|
3
3
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
4
4
|
//#endregion
|
|
5
|
-
//#region context/
|
|
5
|
+
//#region context/RouterProviderContext.ts
|
|
6
|
+
var PropsContext = createContext({});
|
|
6
7
|
var ActionsContext = createContext({});
|
|
7
8
|
var DataContext = createContext({});
|
|
8
9
|
var NavigationContext = createContext({});
|
|
@@ -40,30 +41,37 @@ var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((expor
|
|
|
40
41
|
exports.jsxs = jsxProd;
|
|
41
42
|
}));
|
|
42
43
|
//#endregion
|
|
43
|
-
//#region provider/
|
|
44
|
+
//#region provider/Provider.tsx
|
|
44
45
|
var import_jsx_runtime = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
45
46
|
module.exports = require_react_jsx_runtime_production();
|
|
46
47
|
})))();
|
|
47
|
-
var
|
|
48
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
48
|
+
var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocation, location, prefetchLoader, loaderCache, blockerState, routeList, shouldErrorElementShown, isLoading, isAnimated }) => {
|
|
49
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PropsContext.Provider, {
|
|
49
50
|
value: {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
prefetchLoader,
|
|
53
|
-
setContext
|
|
51
|
+
routeList,
|
|
52
|
+
isAnimated
|
|
54
53
|
},
|
|
55
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
54
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionsContext.Provider, {
|
|
56
55
|
value: {
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
updateLocation,
|
|
57
|
+
updateBlockedRoute,
|
|
58
|
+
prefetchLoader,
|
|
59
|
+
setContext
|
|
59
60
|
},
|
|
60
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
61
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DataContext.Provider, {
|
|
61
62
|
value: {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
location
|
|
63
|
+
context,
|
|
64
|
+
loaderCache
|
|
65
65
|
},
|
|
66
|
-
children
|
|
66
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NavigationContext.Provider, {
|
|
67
|
+
value: {
|
|
68
|
+
blockerState,
|
|
69
|
+
location,
|
|
70
|
+
shouldErrorElementShown,
|
|
71
|
+
isLoading
|
|
72
|
+
},
|
|
73
|
+
children
|
|
74
|
+
})
|
|
67
75
|
})
|
|
68
76
|
})
|
|
69
77
|
});
|
|
@@ -141,8 +149,9 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
|
|
|
141
149
|
const navigation = useCallback((nextLocation) => {
|
|
142
150
|
setLocation(nextLocation);
|
|
143
151
|
prevPathname.current = nextLocation.pathname;
|
|
144
|
-
|
|
145
|
-
|
|
152
|
+
const fullPath = nextLocation.search ? `${nextLocation.pathname}${nextLocation.search}` : nextLocation.pathname;
|
|
153
|
+
if (fullPath === window.location.pathname + window.location.search) return;
|
|
154
|
+
history.pushState(null, "", fullPath);
|
|
146
155
|
}, [setLocation]);
|
|
147
156
|
const transitionedNavigation = useCallback(({ nextLocation, isFirstCall, isAnimated }) => {
|
|
148
157
|
if (isAnimated && !isFirstCall) try {
|
|
@@ -161,9 +170,10 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
|
|
|
161
170
|
pathname: nextLocation.pathname
|
|
162
171
|
});
|
|
163
172
|
if (nextItem?.beforeLoad) try {
|
|
173
|
+
const redirect = async (location) => typeof location === "string" ? await navigationHandler({ pathname: location }) : await navigationHandler(location);
|
|
164
174
|
await nextItem.beforeLoad({
|
|
165
175
|
context,
|
|
166
|
-
redirect
|
|
176
|
+
redirect,
|
|
167
177
|
params
|
|
168
178
|
});
|
|
169
179
|
} catch {
|
|
@@ -264,7 +274,9 @@ var useLoader = ({ routeList, context }) => {
|
|
|
264
274
|
const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
|
|
265
275
|
if (!routeItem) return true;
|
|
266
276
|
const currentCacheTimestamp = cacheTimestampsRef.current[pathname];
|
|
267
|
-
|
|
277
|
+
if (!currentCacheTimestamp) return false;
|
|
278
|
+
if (!routeItem.staleTime) return true;
|
|
279
|
+
return Date.now() - currentCacheTimestamp < routeItem.staleTime;
|
|
268
280
|
}, []);
|
|
269
281
|
const revalidateCache = useCallback(async ({ routeItem, isCurrentRoute, pathname }) => {
|
|
270
282
|
if (!routeItem?.loader) return;
|
|
@@ -322,7 +334,7 @@ var useLoader = ({ routeList, context }) => {
|
|
|
322
334
|
};
|
|
323
335
|
//#endregion
|
|
324
336
|
//#region hooks/useApplyCustomAnimation.ts
|
|
325
|
-
var useApplyCustomAnimation = (
|
|
337
|
+
var useApplyCustomAnimation = (animationDuration) => {
|
|
326
338
|
useEffect(() => {
|
|
327
339
|
const style = document.createElement("style");
|
|
328
340
|
style.id = "spinner-style";
|
|
@@ -349,51 +361,61 @@ var useApplyCustomAnimation = (animationOptions) => {
|
|
|
349
361
|
return () => style.remove();
|
|
350
362
|
}, []);
|
|
351
363
|
useEffect(() => {
|
|
352
|
-
if (!
|
|
364
|
+
if (!animationDuration) return;
|
|
353
365
|
const style = document.createElement("style");
|
|
354
366
|
style.id = "dynamic-view-transition-duration-style";
|
|
355
|
-
style.textContent = `::view-transition-group(
|
|
356
|
-
document.head.appendChild(style);
|
|
357
|
-
return () => style.remove();
|
|
358
|
-
}, [animationOptions]);
|
|
359
|
-
useEffect(() => {
|
|
360
|
-
if (!animationOptions.name) return;
|
|
361
|
-
const style = document.createElement("style");
|
|
362
|
-
style.id = "dynamic-view-transition-duration-name";
|
|
363
|
-
style.textContent = `
|
|
364
|
-
@keyframes fade-out {
|
|
365
|
-
from { opacity: 1; }
|
|
366
|
-
to { opacity: 0; }
|
|
367
|
-
}
|
|
368
|
-
@keyframes fade-in {
|
|
369
|
-
from { opacity: 0; }
|
|
370
|
-
to { opacity: 1; }
|
|
371
|
-
}
|
|
372
|
-
@keyframes slide-left-out {
|
|
373
|
-
from { transform: translateX(0); }
|
|
374
|
-
to { transform: translateX(-100%); }
|
|
375
|
-
}
|
|
376
|
-
@keyframes slide-left-in {
|
|
377
|
-
from { transform: translateX(100%); }
|
|
378
|
-
to { transform: translateX(0); }
|
|
379
|
-
}
|
|
380
|
-
@keyframes slide-right-out {
|
|
381
|
-
from { transform: translateX(0); }
|
|
382
|
-
to { transform: translateX(100%); }
|
|
383
|
-
}
|
|
384
|
-
@keyframes slide-right-in {
|
|
385
|
-
from { transform: translateX(-100%); }
|
|
386
|
-
to { transform: translateX(0); }
|
|
387
|
-
}
|
|
388
|
-
::view-transition-old(root) {
|
|
389
|
-
animation: ${animationOptions.name}-out ${animationOptions.duration ?? 800}ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
390
|
-
}
|
|
391
|
-
::view-transition-new(root) {
|
|
392
|
-
animation: ${animationOptions.name}-in ${animationOptions.duration ?? 800}ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
393
|
-
}`;
|
|
367
|
+
style.textContent = `::view-transition-group(page) { animation-duration: ${animationDuration}ms; }`;
|
|
394
368
|
document.head.appendChild(style);
|
|
395
369
|
return () => style.remove();
|
|
396
|
-
}, [
|
|
370
|
+
}, [animationDuration]);
|
|
371
|
+
};
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region components/RouterProvider.tsx
|
|
374
|
+
var RouterProvider = ({ children, routeList, context: initialContext = {}, isAnimated = false, animationDuration }) => {
|
|
375
|
+
const [location, setLocation] = useState({});
|
|
376
|
+
const [context, setContext] = useState(initialContext);
|
|
377
|
+
useApplyCustomAnimation(animationDuration);
|
|
378
|
+
const { loaderError, loaderCache, prefetchLoader, revalidateCache, isLoading } = useLoader({
|
|
379
|
+
routeList,
|
|
380
|
+
context
|
|
381
|
+
});
|
|
382
|
+
const { blockerState, updateLocation, updateBlockedRoute, beforeLoadError } = useHandleNavigation({
|
|
383
|
+
setLocation,
|
|
384
|
+
routeList,
|
|
385
|
+
context,
|
|
386
|
+
revalidateCache,
|
|
387
|
+
isAnimated
|
|
388
|
+
});
|
|
389
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
|
|
390
|
+
...useMemo(() => ({
|
|
391
|
+
location,
|
|
392
|
+
updateLocation,
|
|
393
|
+
loaderCache,
|
|
394
|
+
prefetchLoader,
|
|
395
|
+
updateBlockedRoute,
|
|
396
|
+
blockerState,
|
|
397
|
+
context,
|
|
398
|
+
setContext,
|
|
399
|
+
routeList,
|
|
400
|
+
isLoading,
|
|
401
|
+
shouldErrorElementShown: loaderError || beforeLoadError,
|
|
402
|
+
isAnimated
|
|
403
|
+
}), [
|
|
404
|
+
beforeLoadError,
|
|
405
|
+
blockerState,
|
|
406
|
+
context,
|
|
407
|
+
isLoading,
|
|
408
|
+
loaderCache,
|
|
409
|
+
loaderError,
|
|
410
|
+
location,
|
|
411
|
+
prefetchLoader,
|
|
412
|
+
routeList,
|
|
413
|
+
updateBlockedRoute,
|
|
414
|
+
updateLocation,
|
|
415
|
+
isAnimated
|
|
416
|
+
]),
|
|
417
|
+
children
|
|
418
|
+
});
|
|
397
419
|
};
|
|
398
420
|
//#endregion
|
|
399
421
|
//#region components/Spinner.tsx
|
|
@@ -405,13 +427,34 @@ var renderElement = (Component) => {
|
|
|
405
427
|
return typeof Component === "function" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, {}) : Component;
|
|
406
428
|
};
|
|
407
429
|
//#endregion
|
|
430
|
+
//#region hooks/useServiceContext.ts
|
|
431
|
+
var useServiceState = (reactContext) => {
|
|
432
|
+
const context = useContext(reactContext);
|
|
433
|
+
if (!Object.keys(context).length) throw new Error("hooks and Router component must be used within RouterProvider");
|
|
434
|
+
return context;
|
|
435
|
+
};
|
|
436
|
+
var useNavigationState = () => useServiceState(NavigationContext);
|
|
437
|
+
var useRouterActions = () => useServiceState(ActionsContext);
|
|
438
|
+
var useRouterData = () => useServiceState(DataContext);
|
|
439
|
+
var usePropsData = () => useServiceState(PropsContext);
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region context/RouterViewContext.ts
|
|
442
|
+
var RouterViewContext = createContext({});
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region provider/ViewProvider.tsx
|
|
445
|
+
var ViewProvider = ({ children, params }) => {
|
|
446
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterViewContext.Provider, {
|
|
447
|
+
value: params,
|
|
448
|
+
children
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
//#endregion
|
|
408
452
|
//#region components/Router.tsx
|
|
409
453
|
var PAGE_NOT_FOUND = "error 404. Page not found";
|
|
410
454
|
var ALL_LOCATIONS = "*";
|
|
411
|
-
var Router = ({
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
useApplyCustomAnimation(animationOptions);
|
|
455
|
+
var Router = ({ spinner = true }) => {
|
|
456
|
+
const { location, isLoading, shouldErrorElementShown } = useNavigationState();
|
|
457
|
+
const { routeList, isAnimated } = usePropsData();
|
|
415
458
|
const routeItem = useMemo(() => {
|
|
416
459
|
if (!location.pathname) return void 0;
|
|
417
460
|
return routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, location.pathname));
|
|
@@ -420,74 +463,41 @@ var Router = ({ routeList, context: initialContext = {}, isAnimated = false, ani
|
|
|
420
463
|
routeItem,
|
|
421
464
|
pathname: window.location.pathname
|
|
422
465
|
}), [routeItem]);
|
|
423
|
-
|
|
424
|
-
routeList,
|
|
425
|
-
context
|
|
426
|
-
});
|
|
427
|
-
const { blockerState, updateLocation, updateBlockedRoute, beforeLoadError } = useHandleNavigation({
|
|
428
|
-
setLocation,
|
|
429
|
-
routeList,
|
|
430
|
-
context,
|
|
431
|
-
revalidateCache,
|
|
432
|
-
isAnimated
|
|
433
|
-
});
|
|
434
|
-
const providerProps = useMemo(() => ({
|
|
435
|
-
location,
|
|
436
|
-
updateLocation,
|
|
437
|
-
params,
|
|
438
|
-
loaderCache,
|
|
439
|
-
prefetchLoader,
|
|
440
|
-
updateBlockedRoute,
|
|
441
|
-
blockerState,
|
|
442
|
-
context,
|
|
443
|
-
setContext
|
|
444
|
-
}), [
|
|
445
|
-
blockerState,
|
|
446
|
-
loaderCache,
|
|
447
|
-
location,
|
|
448
|
-
params,
|
|
449
|
-
prefetchLoader,
|
|
450
|
-
context,
|
|
451
|
-
updateBlockedRoute,
|
|
452
|
-
updateLocation
|
|
453
|
-
]);
|
|
466
|
+
if (spinner && !routeItem && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
|
|
454
467
|
if (!routeItem && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
|
|
455
468
|
if (!routeItem) return null;
|
|
456
|
-
if (!isAnimated && routeItem?.loader && !
|
|
457
|
-
|
|
469
|
+
if (!isAnimated && routeItem?.loader && !shouldErrorElementShown && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewProvider, {
|
|
470
|
+
params,
|
|
458
471
|
children: renderElement(routeItem?.loaderFallback)
|
|
459
472
|
});
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
children: [renderElement(routeItem?.errorElement), isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
|
|
473
|
+
if (shouldErrorElementShown) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ViewProvider, {
|
|
474
|
+
params,
|
|
475
|
+
children: [renderElement(routeItem?.errorElement), spinner && isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
|
|
463
476
|
});
|
|
464
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.
|
|
465
|
-
|
|
466
|
-
children:
|
|
477
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
478
|
+
style: { viewTransitionName: "page" },
|
|
479
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ViewProvider, {
|
|
480
|
+
params,
|
|
481
|
+
children: [renderElement(routeItem?.element) || PAGE_NOT_FOUND, spinner && isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
|
|
482
|
+
})
|
|
467
483
|
});
|
|
468
484
|
};
|
|
469
485
|
//#endregion
|
|
470
|
-
//#region hooks/
|
|
471
|
-
var
|
|
472
|
-
|
|
473
|
-
if (!Object.keys(context).length) throw new Error("useNavigationState must be used within Router component");
|
|
474
|
-
return context;
|
|
475
|
-
};
|
|
476
|
-
var useRouterActions = () => {
|
|
477
|
-
const context = useContext(ActionsContext);
|
|
478
|
-
if (!Object.keys(context).length) throw new Error("useRouterActions must be used within Router component");
|
|
479
|
-
return context;
|
|
480
|
-
};
|
|
481
|
-
var useRouterData = () => {
|
|
482
|
-
const context = useContext(DataContext);
|
|
483
|
-
if (!Object.keys(context).length) throw new Error("useRouterData must be used within Router component");
|
|
484
|
-
return context;
|
|
486
|
+
//#region hooks/useLocation.ts
|
|
487
|
+
var useLocation = () => {
|
|
488
|
+
return useNavigationState().location;
|
|
485
489
|
};
|
|
486
490
|
//#endregion
|
|
487
491
|
//#region hooks/useNavigate.ts
|
|
488
492
|
var useNavigate = () => {
|
|
489
493
|
const { updateLocation } = useRouterActions();
|
|
490
|
-
|
|
494
|
+
const locationRef = useLatest(useLocation());
|
|
495
|
+
return useCallback(async (arg) => {
|
|
496
|
+
if (typeof arg === "number") return history.go(arg);
|
|
497
|
+
if (typeof arg === "string") {
|
|
498
|
+
if (arg !== locationRef.current.pathname) await updateLocation({ pathname: arg });
|
|
499
|
+
} else if (JSON.stringify(arg) !== JSON.stringify(locationRef.current)) await updateLocation(arg);
|
|
500
|
+
}, [updateLocation, locationRef]);
|
|
491
501
|
};
|
|
492
502
|
//#endregion
|
|
493
503
|
//#region components/Link.tsx
|
|
@@ -496,7 +506,7 @@ var Link = ({ children, to, prefetch = true }) => {
|
|
|
496
506
|
const navigate = useNavigate();
|
|
497
507
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
|
|
498
508
|
style: { cursor: "pointer" },
|
|
499
|
-
onClick: () => navigate(
|
|
509
|
+
onClick: () => navigate(to),
|
|
500
510
|
onMouseOver: () => prefetch && prefetchLoader(to),
|
|
501
511
|
children
|
|
502
512
|
});
|
|
@@ -504,13 +514,7 @@ var Link = ({ children, to, prefetch = true }) => {
|
|
|
504
514
|
//#endregion
|
|
505
515
|
//#region hooks/useParams.ts
|
|
506
516
|
var useParams = () => {
|
|
507
|
-
|
|
508
|
-
return params;
|
|
509
|
-
};
|
|
510
|
-
//#endregion
|
|
511
|
-
//#region hooks/useLocation.ts
|
|
512
|
-
var useLocation = () => {
|
|
513
|
-
return useNavigationState().location;
|
|
517
|
+
return useContext(RouterViewContext);
|
|
514
518
|
};
|
|
515
519
|
//#endregion
|
|
516
520
|
//#region hooks/useLoaderState.ts
|
|
@@ -562,4 +566,38 @@ var useRouterContext = () => {
|
|
|
562
566
|
};
|
|
563
567
|
};
|
|
564
568
|
//#endregion
|
|
565
|
-
|
|
569
|
+
//#region hooks/useSearchParams.ts
|
|
570
|
+
var useSearchParams = () => {
|
|
571
|
+
const location = useLocation();
|
|
572
|
+
const navigate = useNavigate();
|
|
573
|
+
const locationRef = useLatest(location);
|
|
574
|
+
const searchString = useMemo(() => location.search ? location.search.replace("?", "") : location.pathname.split("?")?.[1] ?? "", [location.pathname, location.search]);
|
|
575
|
+
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
|
|
576
|
+
const getSearchParams = useCallback((param) => {
|
|
577
|
+
const allValues = searchParams.getAll(param);
|
|
578
|
+
return allValues.length > 1 ? allValues : allValues[0] ?? "";
|
|
579
|
+
}, [searchParams]);
|
|
580
|
+
const navigateWithSearchParams = useCallback((params) => {
|
|
581
|
+
const newSearch = params.toString();
|
|
582
|
+
navigate({
|
|
583
|
+
pathname: locationRef.current.pathname,
|
|
584
|
+
search: newSearch ? `?${newSearch}` : ""
|
|
585
|
+
});
|
|
586
|
+
}, [locationRef, navigate]);
|
|
587
|
+
return {
|
|
588
|
+
searchParams,
|
|
589
|
+
getSearchParams,
|
|
590
|
+
setSearchParams: useCallback((param, value) => {
|
|
591
|
+
const search = locationRef.current.search || "";
|
|
592
|
+
const currentParams = new URLSearchParams(search);
|
|
593
|
+
if (typeof param === "string" && value !== void 0) {
|
|
594
|
+
currentParams.delete(param);
|
|
595
|
+
(Array.isArray(value) ? value : [value]).forEach((v) => currentParams.append(param, v));
|
|
596
|
+
navigateWithSearchParams(currentParams);
|
|
597
|
+
} else if (typeof param === "function") navigateWithSearchParams(param(currentParams));
|
|
598
|
+
else throw new Error("useSearchParams first argument must be either function or string");
|
|
599
|
+
}, [locationRef, navigateWithSearchParams])
|
|
600
|
+
};
|
|
601
|
+
};
|
|
602
|
+
//#endregion
|
|
603
|
+
export { Link, Router, RouterProvider, createRouter, useBeforeUnload, useBlocker, useLoaderState, useLocation, useNavigate, useParams, useRouterContext, useSearchParams };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type ActionsContextValue, type DataContextValue, type NavigationContextValue, PropsContextValue } from '../context/RouterProviderContext';
|
|
3
|
+
type ProviderProps = PropsContextValue & NavigationContextValue & ActionsContextValue & DataContextValue & {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
};
|
|
6
|
+
export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation, location, prefetchLoader, loaderCache, blockerState, routeList, shouldErrorElementShown, isLoading, isAnimated, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
package/dist/types/global.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type ClientRouteItem = {
|
|
|
16
16
|
staleTime?: number;
|
|
17
17
|
beforeLoad?: (arg: {
|
|
18
18
|
context: Record<string, unknown>;
|
|
19
|
-
redirect: (arg: Location) => Promise<void>;
|
|
19
|
+
redirect: (arg: Location | string) => Promise<void>;
|
|
20
20
|
params: Record<string, string>;
|
|
21
21
|
}) => Promise<unknown> | undefined;
|
|
22
22
|
afterLoad?: (arg: {
|
|
@@ -42,10 +42,6 @@ export type UpdateBlockedRouteProps = {
|
|
|
42
42
|
type: 'process' | 'reset' | 'charge' | 'unblock';
|
|
43
43
|
payload?: string;
|
|
44
44
|
};
|
|
45
|
-
export type AnimationOptions = {
|
|
46
|
-
duration?: number;
|
|
47
|
-
name?: 'fade' | 'slide-left' | 'slide-right';
|
|
48
|
-
};
|
|
49
45
|
export type RevalidateCacheArgs = {
|
|
50
46
|
pathname: string;
|
|
51
47
|
routeItem?: RouteItem;
|
package/package.json
CHANGED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
import { type ActionsContextValue, type DataContextValue, type NavigationContextValue } from '../context/RouterContext';
|
|
3
|
-
type RouterProviderProps = NavigationContextValue & ActionsContextValue & DataContextValue & {
|
|
4
|
-
children: ReactNode;
|
|
5
|
-
};
|
|
6
|
-
export declare const RouterProvider: ({ children, setContext, context, updateBlockedRoute, updateLocation, location, params, prefetchLoader, loaderCache, blockerState, }: RouterProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
-
export {};
|