clear-react-router 1.2.5 → 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 -128
- 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(
|
|
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
|
-
}`;
|
|
366
|
+
style.textContent = `::view-transition-group(page) { animation-duration: ${animationDuration}ms; }`;
|
|
394
367
|
document.head.appendChild(style);
|
|
395
368
|
return () => style.remove();
|
|
396
|
-
}, [
|
|
369
|
+
}, [animationDuration]);
|
|
397
370
|
};
|
|
398
371
|
//#endregion
|
|
399
|
-
//#region components/
|
|
400
|
-
var
|
|
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;
|
|
406
|
-
};
|
|
407
|
-
//#endregion
|
|
408
|
-
//#region components/Router.tsx
|
|
409
|
-
var PAGE_NOT_FOUND = "error 404. Page not found";
|
|
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,63 +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
|
-
children
|
|
463
|
-
});
|
|
464
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RouterProvider, {
|
|
465
|
-
...providerProps,
|
|
466
|
-
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
|
|
467
417
|
});
|
|
468
418
|
};
|
|
469
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
|
|
470
429
|
//#region hooks/useServiceContext.ts
|
|
471
430
|
var useNavigationState = () => {
|
|
472
431
|
const context = useContext(NavigationContext);
|
|
473
|
-
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");
|
|
474
433
|
return context;
|
|
475
434
|
};
|
|
476
435
|
var useRouterActions = () => {
|
|
477
436
|
const context = useContext(ActionsContext);
|
|
478
|
-
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");
|
|
479
438
|
return context;
|
|
480
439
|
};
|
|
481
440
|
var useRouterData = () => {
|
|
482
441
|
const context = useContext(DataContext);
|
|
483
|
-
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");
|
|
484
443
|
return context;
|
|
485
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
|
+
};
|
|
486
500
|
//#endregion
|
|
487
501
|
//#region hooks/useNavigate.ts
|
|
488
502
|
var useNavigate = () => {
|
|
489
503
|
const { updateLocation } = useRouterActions();
|
|
490
|
-
|
|
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]);
|
|
491
511
|
};
|
|
492
512
|
//#endregion
|
|
493
513
|
//#region components/Link.tsx
|
|
@@ -496,7 +516,7 @@ var Link = ({ children, to, prefetch = true }) => {
|
|
|
496
516
|
const navigate = useNavigate();
|
|
497
517
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
|
|
498
518
|
style: { cursor: "pointer" },
|
|
499
|
-
onClick: () => navigate(
|
|
519
|
+
onClick: () => navigate(to),
|
|
500
520
|
onMouseOver: () => prefetch && prefetchLoader(to),
|
|
501
521
|
children
|
|
502
522
|
});
|
|
@@ -504,13 +524,7 @@ var Link = ({ children, to, prefetch = true }) => {
|
|
|
504
524
|
//#endregion
|
|
505
525
|
//#region hooks/useParams.ts
|
|
506
526
|
var useParams = () => {
|
|
507
|
-
|
|
508
|
-
return params;
|
|
509
|
-
};
|
|
510
|
-
//#endregion
|
|
511
|
-
//#region hooks/useLocation.ts
|
|
512
|
-
var useLocation = () => {
|
|
513
|
-
return useNavigationState().location;
|
|
527
|
+
return useContext(RouterViewContext);
|
|
514
528
|
};
|
|
515
529
|
//#endregion
|
|
516
530
|
//#region hooks/useLoaderState.ts
|
|
@@ -562,4 +576,4 @@ var useRouterContext = () => {
|
|
|
562
576
|
};
|
|
563
577
|
};
|
|
564
578
|
//#endregion
|
|
565
|
-
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 {};
|