clear-react-router 1.2.4 → 1.3.0
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 +87 -46
- 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/useServiceContext.d.ts +4 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +142 -127
- 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
|
+
```
|
|
47
63
|
|
|
48
|
-
|
|
64
|
+
### `Router`
|
|
65
|
+
|
|
66
|
+
Renders the current route's component. Must be placed inside `<RouterProvider>`.
|
|
67
|
+
|
|
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) |
|
|
49
71
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
```
|
|
73
|
+
<RouterProvider routeList={routes} isAnimated>
|
|
74
|
+
<Navbar />
|
|
75
|
+
<Router spinner={false} /> {/* disable the spinner */}
|
|
76
|
+
</RouterProvider>
|
|
77
|
+
```
|
|
54
78
|
|
|
55
|
-
> **Note:** When `isAnimated` is enabled,
|
|
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
|
-
|
|
83
|
-
|
|
105
|
+
if (!context.isAuthorized) return redirect('/');
|
|
106
|
+
},
|
|
107
|
+
},
|
|
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' } });
|
|
84
118
|
},
|
|
85
119
|
},
|
|
86
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,7 +266,7 @@ 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' } });
|
|
@@ -239,35 +291,24 @@ Clear Router supports smooth page transitions using the native View Transitions
|
|
|
239
291
|
```
|
|
240
292
|
import { Router } from 'clear-react-router';
|
|
241
293
|
|
|
242
|
-
// Enable
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
name: 'slide-left' // 'fade' | 'slide-left' | 'slide-right'
|
|
252
|
-
}}
|
|
253
|
-
/>
|
|
294
|
+
// Enable fade animation
|
|
295
|
+
<RouterProvider routeList={routes} isAnimated>
|
|
296
|
+
<Router />
|
|
297
|
+
</RouterProvider>
|
|
298
|
+
|
|
299
|
+
// Custom animation duration
|
|
300
|
+
<RouterProvider routeList={routes} isAnimated animationDuration={800}>
|
|
301
|
+
<Router />
|
|
302
|
+
</RouterProvider>
|
|
254
303
|
```
|
|
255
304
|
|
|
256
305
|
## How It Works
|
|
257
306
|
|
|
258
307
|
- **Data loads first** — All `loader` and `beforeLoad` hooks complete before animation starts
|
|
259
308
|
- **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
|
|
309
|
+
- **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
310
|
- **Native API** — Uses `document.startViewTransition` for smooth, hardware-accelerated animations
|
|
262
311
|
|
|
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
312
|
## Browser Support
|
|
272
313
|
|
|
273
314
|
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>;
|
|
@@ -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
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
|
});
|
|
@@ -161,9 +169,10 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
|
|
|
161
169
|
pathname: nextLocation.pathname
|
|
162
170
|
});
|
|
163
171
|
if (nextItem?.beforeLoad) try {
|
|
172
|
+
const redirect = async (location) => typeof location === "string" ? await navigationHandler({ pathname: location }) : await navigationHandler(location);
|
|
164
173
|
await nextItem.beforeLoad({
|
|
165
174
|
context,
|
|
166
|
-
redirect
|
|
175
|
+
redirect,
|
|
167
176
|
params
|
|
168
177
|
});
|
|
169
178
|
} catch {
|
|
@@ -264,7 +273,9 @@ var useLoader = ({ routeList, context }) => {
|
|
|
264
273
|
const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
|
|
265
274
|
if (!routeItem) return true;
|
|
266
275
|
const currentCacheTimestamp = cacheTimestampsRef.current[pathname];
|
|
267
|
-
|
|
276
|
+
if (!currentCacheTimestamp) return false;
|
|
277
|
+
if (!routeItem.staleTime) return true;
|
|
278
|
+
return Date.now() - currentCacheTimestamp < routeItem.staleTime;
|
|
268
279
|
}, []);
|
|
269
280
|
const revalidateCache = useCallback(async ({ routeItem, isCurrentRoute, pathname }) => {
|
|
270
281
|
if (!routeItem?.loader) return;
|
|
@@ -322,7 +333,7 @@ var useLoader = ({ routeList, context }) => {
|
|
|
322
333
|
};
|
|
323
334
|
//#endregion
|
|
324
335
|
//#region hooks/useApplyCustomAnimation.ts
|
|
325
|
-
var useApplyCustomAnimation = (
|
|
336
|
+
var useApplyCustomAnimation = (animationDuration) => {
|
|
326
337
|
useEffect(() => {
|
|
327
338
|
const style = document.createElement("style");
|
|
328
339
|
style.id = "spinner-style";
|
|
@@ -349,77 +360,20 @@ var useApplyCustomAnimation = (animationOptions) => {
|
|
|
349
360
|
return () => style.remove();
|
|
350
361
|
}, []);
|
|
351
362
|
useEffect(() => {
|
|
352
|
-
if (!
|
|
363
|
+
if (!animationDuration) return;
|
|
353
364
|
const style = document.createElement("style");
|
|
354
365
|
style.id = "dynamic-view-transition-duration-style";
|
|
355
|
-
style.textContent = `::view-transition-group(
|
|
366
|
+
style.textContent = `::view-transition-group(page) { animation-duration: ${animationDuration}ms; }`;
|
|
356
367
|
document.head.appendChild(style);
|
|
357
368
|
return () => style.remove();
|
|
358
|
-
}, [
|
|
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
|
-
}`;
|
|
394
|
-
document.head.appendChild(style);
|
|
395
|
-
return () => style.remove();
|
|
396
|
-
}, [animationOptions]);
|
|
397
|
-
};
|
|
398
|
-
//#endregion
|
|
399
|
-
//#region components/Spinner.tsx
|
|
400
|
-
var Spinner = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cr-spinner" });
|
|
401
|
-
//#endregion
|
|
402
|
-
//#region utils/renderElement.tsx
|
|
403
|
-
var renderElement = (Component) => {
|
|
404
|
-
if (!Component) return null;
|
|
405
|
-
return typeof Component === "function" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, {}) : Component;
|
|
369
|
+
}, [animationDuration]);
|
|
406
370
|
};
|
|
407
371
|
//#endregion
|
|
408
|
-
//#region components/
|
|
409
|
-
var
|
|
410
|
-
var ALL_LOCATIONS = "*";
|
|
411
|
-
var Router = ({ routeList, context: initialContext = {}, isAnimated = false, animationOptions = {} }) => {
|
|
372
|
+
//#region components/RouterProvider.tsx
|
|
373
|
+
var RouterProvider = ({ children, routeList, context: initialContext = {}, isAnimated = false, animationDuration }) => {
|
|
412
374
|
const [location, setLocation] = useState({});
|
|
413
375
|
const [context, setContext] = useState(initialContext);
|
|
414
|
-
useApplyCustomAnimation(
|
|
415
|
-
const routeItem = useMemo(() => {
|
|
416
|
-
if (!location.pathname) return void 0;
|
|
417
|
-
return routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, location.pathname));
|
|
418
|
-
}, [location.pathname, routeList]);
|
|
419
|
-
const params = useMemo(() => getParamsObject({
|
|
420
|
-
routeItem,
|
|
421
|
-
pathname: window.location.pathname
|
|
422
|
-
}), [routeItem]);
|
|
376
|
+
useApplyCustomAnimation(animationDuration);
|
|
423
377
|
const { loaderError, loaderCache, prefetchLoader, revalidateCache, isLoading } = useLoader({
|
|
424
378
|
routeList,
|
|
425
379
|
context
|
|
@@ -431,62 +385,129 @@ var Router = ({ routeList, context: initialContext = {}, isAnimated = false, ani
|
|
|
431
385
|
revalidateCache,
|
|
432
386
|
isAnimated
|
|
433
387
|
});
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RouterProvider, {
|
|
464
|
-
...providerProps,
|
|
465
|
-
children: [renderElement(routeItem?.element) || PAGE_NOT_FOUND, isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
|
|
388
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
|
|
389
|
+
...useMemo(() => ({
|
|
390
|
+
location,
|
|
391
|
+
updateLocation,
|
|
392
|
+
loaderCache,
|
|
393
|
+
prefetchLoader,
|
|
394
|
+
updateBlockedRoute,
|
|
395
|
+
blockerState,
|
|
396
|
+
context,
|
|
397
|
+
setContext,
|
|
398
|
+
routeList,
|
|
399
|
+
isLoading,
|
|
400
|
+
shouldErrorElementShown: loaderError || beforeLoadError,
|
|
401
|
+
isAnimated
|
|
402
|
+
}), [
|
|
403
|
+
beforeLoadError,
|
|
404
|
+
blockerState,
|
|
405
|
+
context,
|
|
406
|
+
isLoading,
|
|
407
|
+
loaderCache,
|
|
408
|
+
loaderError,
|
|
409
|
+
location,
|
|
410
|
+
prefetchLoader,
|
|
411
|
+
routeList,
|
|
412
|
+
updateBlockedRoute,
|
|
413
|
+
updateLocation,
|
|
414
|
+
isAnimated
|
|
415
|
+
]),
|
|
416
|
+
children
|
|
466
417
|
});
|
|
467
418
|
};
|
|
468
419
|
//#endregion
|
|
420
|
+
//#region components/Spinner.tsx
|
|
421
|
+
var Spinner = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cr-spinner" });
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region utils/renderElement.tsx
|
|
424
|
+
var renderElement = (Component) => {
|
|
425
|
+
if (!Component) return null;
|
|
426
|
+
return typeof Component === "function" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, {}) : Component;
|
|
427
|
+
};
|
|
428
|
+
//#endregion
|
|
469
429
|
//#region hooks/useServiceContext.ts
|
|
470
430
|
var useNavigationState = () => {
|
|
471
431
|
const context = useContext(NavigationContext);
|
|
472
|
-
if (!Object.keys(context).length) throw new Error("
|
|
432
|
+
if (!Object.keys(context).length) throw new Error("hooks and Router component must be used within RouterProvider");
|
|
473
433
|
return context;
|
|
474
434
|
};
|
|
475
435
|
var useRouterActions = () => {
|
|
476
436
|
const context = useContext(ActionsContext);
|
|
477
|
-
if (!Object.keys(context).length) throw new Error("
|
|
437
|
+
if (!Object.keys(context).length) throw new Error("hooks and Router component must be used within RouterProvider");
|
|
478
438
|
return context;
|
|
479
439
|
};
|
|
480
440
|
var useRouterData = () => {
|
|
481
441
|
const context = useContext(DataContext);
|
|
482
|
-
if (!Object.keys(context).length) throw new Error("
|
|
442
|
+
if (!Object.keys(context).length) throw new Error("hooks and Router component must be used within RouterProvider");
|
|
483
443
|
return context;
|
|
484
444
|
};
|
|
445
|
+
var usePropsData = () => {
|
|
446
|
+
const context = useContext(PropsContext);
|
|
447
|
+
if (!Object.keys(context).length) throw new Error("hooks and Router component must be used within RouterProvider");
|
|
448
|
+
return context;
|
|
449
|
+
};
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region context/RouterViewContext.ts
|
|
452
|
+
var RouterViewContext = createContext({});
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region provider/ViewProvider.tsx
|
|
455
|
+
var ViewProvider = ({ children, params }) => {
|
|
456
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterViewContext.Provider, {
|
|
457
|
+
value: params,
|
|
458
|
+
children
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region components/Router.tsx
|
|
463
|
+
var PAGE_NOT_FOUND = "error 404. Page not found";
|
|
464
|
+
var ALL_LOCATIONS = "*";
|
|
465
|
+
var Router = ({ spinner = true }) => {
|
|
466
|
+
const { location, isLoading, shouldErrorElementShown } = useNavigationState();
|
|
467
|
+
const { routeList, isAnimated } = usePropsData();
|
|
468
|
+
const routeItem = useMemo(() => {
|
|
469
|
+
if (!location.pathname) return void 0;
|
|
470
|
+
return routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, location.pathname));
|
|
471
|
+
}, [location.pathname, routeList]);
|
|
472
|
+
const params = useMemo(() => getParamsObject({
|
|
473
|
+
routeItem,
|
|
474
|
+
pathname: window.location.pathname
|
|
475
|
+
}), [routeItem]);
|
|
476
|
+
if (spinner && !routeItem && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
|
|
477
|
+
if (!routeItem && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
|
|
478
|
+
if (!routeItem) return null;
|
|
479
|
+
if (!isAnimated && routeItem?.loader && !shouldErrorElementShown && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewProvider, {
|
|
480
|
+
params,
|
|
481
|
+
children: renderElement(routeItem?.loaderFallback)
|
|
482
|
+
});
|
|
483
|
+
if (shouldErrorElementShown) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ViewProvider, {
|
|
484
|
+
params,
|
|
485
|
+
children: [renderElement(routeItem?.errorElement), spinner && isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
|
|
486
|
+
});
|
|
487
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
488
|
+
style: { viewTransitionName: "page" },
|
|
489
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ViewProvider, {
|
|
490
|
+
params,
|
|
491
|
+
children: [renderElement(routeItem?.element) || PAGE_NOT_FOUND, spinner && isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
|
|
492
|
+
})
|
|
493
|
+
});
|
|
494
|
+
};
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region hooks/useLocation.ts
|
|
497
|
+
var useLocation = () => {
|
|
498
|
+
return useNavigationState().location;
|
|
499
|
+
};
|
|
485
500
|
//#endregion
|
|
486
501
|
//#region hooks/useNavigate.ts
|
|
487
502
|
var useNavigate = () => {
|
|
488
503
|
const { updateLocation } = useRouterActions();
|
|
489
|
-
|
|
504
|
+
const location = useLocation();
|
|
505
|
+
return useCallback(async (arg) => {
|
|
506
|
+
if (typeof arg === "number") return history.go(arg);
|
|
507
|
+
if (typeof arg === "string") {
|
|
508
|
+
if (arg !== location.pathname) await updateLocation({ pathname: arg });
|
|
509
|
+
} else if (JSON.stringify(arg) !== JSON.stringify(location)) await updateLocation(arg);
|
|
510
|
+
}, [location, updateLocation]);
|
|
490
511
|
};
|
|
491
512
|
//#endregion
|
|
492
513
|
//#region components/Link.tsx
|
|
@@ -495,7 +516,7 @@ var Link = ({ children, to, prefetch = true }) => {
|
|
|
495
516
|
const navigate = useNavigate();
|
|
496
517
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
|
|
497
518
|
style: { cursor: "pointer" },
|
|
498
|
-
onClick: () => navigate(
|
|
519
|
+
onClick: () => navigate(to),
|
|
499
520
|
onMouseOver: () => prefetch && prefetchLoader(to),
|
|
500
521
|
children
|
|
501
522
|
});
|
|
@@ -503,13 +524,7 @@ var Link = ({ children, to, prefetch = true }) => {
|
|
|
503
524
|
//#endregion
|
|
504
525
|
//#region hooks/useParams.ts
|
|
505
526
|
var useParams = () => {
|
|
506
|
-
|
|
507
|
-
return params;
|
|
508
|
-
};
|
|
509
|
-
//#endregion
|
|
510
|
-
//#region hooks/useLocation.ts
|
|
511
|
-
var useLocation = () => {
|
|
512
|
-
return useNavigationState().location;
|
|
527
|
+
return useContext(RouterViewContext);
|
|
513
528
|
};
|
|
514
529
|
//#endregion
|
|
515
530
|
//#region hooks/useLoaderState.ts
|
|
@@ -561,4 +576,4 @@ var useRouterContext = () => {
|
|
|
561
576
|
};
|
|
562
577
|
};
|
|
563
578
|
//#endregion
|
|
564
|
-
export { Link, Router, createRouter, useBeforeUnload, useBlocker, useLoaderState, useLocation, useNavigate, useParams, useRouterContext };
|
|
579
|
+
export { Link, Router, RouterProvider, createRouter, useBeforeUnload, useBlocker, useLoaderState, useLocation, useNavigate, useParams, useRouterContext };
|
|
@@ -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 {};
|