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 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 and slide effects (customizable type and duration)
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` | Cache duration in ms for loader data |
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
- ### `Router`
38
+ ### `RouterProvider`
38
39
 
39
- Main component that renders the application based on current URL.
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` | `{}` | Optional initial context (user, theme, etc.) |
45
+ | `context` | `object` | `{}` | Initial context (user, theme, etc.) |
45
46
  | `isAnimated` | `boolean` | `false` | Enable smooth page transitions |
46
- | `animationOptions` | `AnimationOptions` | `{ duration: 300, name: 'fade' }` | Animation settings (only when `isAnimated` is `true`) |
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
- **`AnimationOptions`:**
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
- | Property | Type | Default | Description |
51
- |----------|------|---------|-------------|
52
- | `duration` | `number` | `300` | Animation duration in milliseconds |
53
- | `name` | `'fade' \| 'slide-left' \| 'slide-right'` | `'fade'` | Type of transition effect |
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, the `loaderFallback` is not displayed. Instead, a small spinner appears in the corner while data loads, ensuring smooth transitions without layout shifts.
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('/');
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({ pathname: '/login' });
141
+ return redirect('/login');
107
142
  }
108
143
  // Validate parameter
109
144
  if (!params.userId || !isValidUserId(params.userId)) {
110
- return redirect({ pathname: '/users' });
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
- - `navigate({ pathname: '/about' })` - navigate to path
128
- - `navigate({ pathname: '/user/123', state: { fromDashboard: true } })` - navigate with state
129
- - `navigate(-1)` - go back
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 loaderState from current route's loader.
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
- const state = useLoaderState();
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
- Handles router context.
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 default fade animation
243
- <Router routeList={routes} isAnimated />
244
-
245
- // Custom animation
246
- <Router
247
- routeList={routes}
248
- isAnimated
249
- animationOptions={{
250
- duration: 500, // milliseconds
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
- import { AnimationOptions, RouteItem } from '../types/global';
2
- type RouterProps = {
3
- routeList: RouteItem[];
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 type { BlockerState, Location, UpdateBlockedRouteProps } from '../types/global';
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
- import { AnimationOptions } from '../types/global';
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.ts';
2
- export declare const useNavigate: () => (arg: Location | -1) => Promise<void>;
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/RouterContext").NavigationContextValue;
2
- export declare const useRouterActions: () => import("../context/RouterContext").ActionsContextValue;
3
- export declare const useRouterData: () => import("../context/RouterContext").DataContextValue;
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';
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/RouterContext.ts
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/RouterProvider.tsx
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 RouterProvider = ({ children, setContext, context, updateBlockedRoute, updateLocation, location, params, prefetchLoader, loaderCache, blockerState }) => {
48
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionsContext.Provider, {
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
- updateLocation,
51
- updateBlockedRoute,
52
- prefetchLoader,
53
- setContext
51
+ routeList,
52
+ isAnimated
54
53
  },
55
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DataContext.Provider, {
54
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionsContext.Provider, {
56
55
  value: {
57
- context,
58
- loaderCache
56
+ updateLocation,
57
+ updateBlockedRoute,
58
+ prefetchLoader,
59
+ setContext
59
60
  },
60
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NavigationContext.Provider, {
61
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DataContext.Provider, {
61
62
  value: {
62
- blockerState,
63
- params,
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: navigationHandler,
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
- return Boolean(currentCacheTimestamp && Date.now() - currentCacheTimestamp < (routeItem.staleTime || 0));
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 = (animationOptions) => {
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 (!animationOptions?.duration) return;
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(root) { animation-duration: ${animationOptions.duration}ms; }`;
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
- }, [animationOptions]);
369
+ }, [animationDuration]);
397
370
  };
398
371
  //#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;
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(animationOptions);
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
- 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
- ]);
454
- if (!routeItem && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
455
- if (!routeItem) return null;
456
- if (!isAnimated && routeItem?.loader && !loaderError && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterProvider, {
457
- ...providerProps,
458
- children: renderElement(routeItem?.loaderFallback)
459
- });
460
- if (loaderError || beforeLoadError) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RouterProvider, {
461
- ...providerProps,
462
- children: [renderElement(routeItem?.errorElement), isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
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("useNavigationState must be used within Router component");
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("useRouterActions must be used within Router component");
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("useRouterData must be used within Router component");
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
- return useCallback(async (arg) => arg === -1 ? history.go(-1) : await updateLocation(arg), [updateLocation]);
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({ pathname: to }),
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
- const { params } = useNavigationState();
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 {};
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from 'react';
2
+ type ViewProviderProps = {
3
+ params: Record<string, string>;
4
+ children: ReactNode;
5
+ };
6
+ export declare const ViewProvider: ({ children, params }: ViewProviderProps) => import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {
@@ -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 {};