clear-react-router 1.2.5 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
+ ```
63
+
64
+ ### `Router`
47
65
 
48
- **`AnimationOptions`:**
66
+ Renders the current route's component. Must be placed inside `<RouterProvider>`.
49
67
 
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 |
68
+ | Prop | Type | Default | Description |
69
+ |------|------|---------|-------------|
70
+ | `spinner` | `boolean /| undefined` | `true` | Show a small spinner in the corner while loading data (only when `isAnimated` is enabled) |
54
71
 
55
- > **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.
72
+ ```
73
+ <RouterProvider routeList={routes} isAnimated>
74
+ <Navbar />
75
+ <Router spinner={false} /> {/* disable the spinner */}
76
+ </RouterProvider>
77
+ ```
78
+
79
+ > **Note:** When `isAnimated` is enabled, `loaderFallback` is not shown. Instead, a small spinner appears (if `spinner={true}`).
56
80
 
57
81
  ### `Link`
58
82
 
@@ -68,7 +92,7 @@ Component for client-side navigation with prefetch support.
68
92
 
69
93
  Function provided to `beforeLoad` for programmatic redirection.
70
94
 
71
- **Type:** `(arg: Location) => Promise<void>`
95
+ **Type:** `(arg: Location | string) => Promise<void>`
72
96
 
73
97
  ```
74
98
  import type { Location } from 'clear-react-router';
@@ -78,12 +102,23 @@ const routes = createRouter([
78
102
  path: '/dashboard',
79
103
  element: <Dashboard />,
80
104
  beforeLoad: ({ context, redirect }) => {
81
- if (!context.isAuthorized) {
82
- return redirect({ pathname: '/' });
83
- }
105
+ if (!context.isAuthorized) return redirect('/');
84
106
  },
85
107
  },
86
108
  ]);
109
+
110
+ or
111
+
112
+ const routes = createRouter([
113
+ {
114
+ path: '/dashboard',
115
+ element: <Dashboard />,
116
+ beforeLoad: ({ context, redirect }) => {
117
+ if (!context.isAuthorized) return redirect({ pathname: '/login', state: { from: '/dashboard' } });
118
+ },
119
+ },
120
+ ]);
121
+
87
122
  ```
88
123
 
89
124
  ### Usage with Parameters
@@ -103,11 +138,11 @@ const routes = createRouter([
103
138
  beforeLoad: async ({ params, context, redirect }) => {
104
139
  // Authentication check
105
140
  if (!context.isAuthorized) {
106
- return redirect({ 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,12 +266,59 @@ 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' } });
221
273
  ```
222
274
 
275
+ ### `useSearchParams()`
276
+
277
+ Returns an object for working with URL query parameters. Supports reading and setting both single values and arrays.
278
+
279
+ ```
280
+ import { useSearchParams } from 'clear-react-router';
281
+
282
+ function ProductFilter() {
283
+ const { searchParams, getSearchParams, setSearchParams } = useSearchParams();
284
+
285
+ // Get a single value or array
286
+ const brand = getSearchParams('brand'); // 'nike' | ['nike', 'reebok'] | ''
287
+
288
+ // Set a single value
289
+ setSearchParams('brand', 'nike'); // ?brand=nike
290
+
291
+ // Set multiple values (array)
292
+ setSearchParams('brand', ['nike', 'reebok']); // ?brand=nike&brand=reebok
293
+
294
+ // Functional update (preserves other params)
295
+ setSearchParams((prev) => {
296
+ prev.set('page', '2');
297
+ prev.append('color', 'red');
298
+ return prev;
299
+ });
300
+
301
+ // Direct access to URLSearchParams
302
+ const allParams = searchParams.toString(); // "brand=nike&brand=reebok&page=2"
303
+ }
304
+ ```
305
+ **Returns:**
306
+
307
+ | Property | Type | Description |
308
+ |----------|------|-------------|
309
+ | `searchParams` | `URLSearchParams` | Raw `URLSearchParams` object for low-level access |
310
+ | `getSearchParams` | `(key: string) => string \| string[]` | Returns a single value or an array if multiple values exist for the key |
311
+ | `setSearchParams` | `(param: string, value: string \| string[]) => void` `or` `(updater: (prev: URLSearchParams) => URLSearchParams) => void` | Update query parameters. Supports single values, arrays, or functional updates |
312
+
313
+ **Key features:**
314
+
315
+ - ✅ **Array support** — `getSearchParams` returns `string[]` when multiple values exist for the same key
316
+ - ✅ **Functional updates** — Update parameters based on previous state without losing other params
317
+ - ✅ **Type-safe** — Proper TypeScript support with overloads
318
+ - ✅ **Stable reference** — `setSearchParams` reference is stable and safe to use in `useEffect`
319
+
320
+ > **Note:** `getSearchParams` returns `string` for single values, `string[]` for multiple values, and `''` if the key is not found.
321
+
223
322
  ## Lazy Loading
224
323
 
225
324
  Clear Router supports code-splitting out of the box. Simply pass a function that returns a dynamic import:
@@ -239,35 +338,24 @@ Clear Router supports smooth page transitions using the native View Transitions
239
338
  ```
240
339
  import { Router } from 'clear-react-router';
241
340
 
242
- // Enable default fade animation
243
- <Router routeList={routes} isAnimated />
341
+ // Enable fade animation
342
+ <RouterProvider routeList={routes} isAnimated>
343
+ <Router />
344
+ </RouterProvider>
244
345
 
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
- />
346
+ // Custom animation duration
347
+ <RouterProvider routeList={routes} isAnimated animationDuration={800}>
348
+ <Router />
349
+ </RouterProvider>
254
350
  ```
255
351
 
256
352
  ## How It Works
257
353
 
258
354
  - **Data loads first** — All `loader` and `beforeLoad` hooks complete before animation starts
259
355
  - **No `loaderFallback`** — The `loaderFallback` is not shown during animated transitions
260
- - **Subtle spinner** — A small spinner appears in the top-left corner while data is loading, so users know the app is responsive
356
+ - **Subtle spinner** — A small spinner appears in the top-left corner while data is loading and `spinner` prop of `Router` component is on, so users know the app is responsive
261
357
  - **Native API** — Uses `document.startViewTransition` for smooth, hardware-accelerated animations
262
358
 
263
- ## Animation Types
264
-
265
- | Name | Effect |
266
- |------|--------|
267
- | `fade` | Cross-fade between pages (default) |
268
- | `slide-left` | New page slides in from right, old page slides out to left |
269
- | `slide-right` | New page slides in from left, old page slides out to right |
270
-
271
359
  ## Browser Support
272
360
 
273
361
  View Transitions API requires modern browsers:
@@ -1,9 +1,3 @@
1
- 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>;
@@ -0,0 +1,10 @@
1
+ type UseSearchParamsReturn = {
2
+ searchParams: URLSearchParams;
3
+ getSearchParams(arg: string): string | string[];
4
+ setSearchParams: {
5
+ (param: string, value: string | string[]): void;
6
+ (param: (prevState: URLSearchParams) => URLSearchParams): void;
7
+ };
8
+ };
9
+ export declare const useSearchParams: () => UseSearchParamsReturn;
10
+ export {};
@@ -1,3 +1,4 @@
1
- export declare const useNavigationState: () => import("../context/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';
@@ -7,5 +8,6 @@ export { useLoaderState } from './hooks/useLoaderState';
7
8
  export { useBlocker } from './hooks/useBlocker';
8
9
  export { useBeforeUnload } from './hooks/useBeforeUnload';
9
10
  export { useRouterContext } from './hooks/useRouterContext';
11
+ export { useSearchParams } from './hooks/useSearchParams';
10
12
  export { createRouter } from './utils/utils';
11
13
  export type { RouteItem, BlockerState, Location } from './types/global';
package/dist/index.js CHANGED
@@ -2,7 +2,8 @@ import { Suspense, createContext, lazy, useCallback, useContext, useEffect, useM
2
2
  //#region \0rolldown/runtime.js
3
3
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
4
4
  //#endregion
5
- //#region context/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
  });
@@ -141,8 +149,9 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
141
149
  const navigation = useCallback((nextLocation) => {
142
150
  setLocation(nextLocation);
143
151
  prevPathname.current = nextLocation.pathname;
144
- if (nextLocation.pathname === window.location.pathname) return;
145
- history.pushState(null, "", nextLocation.pathname);
152
+ const fullPath = nextLocation.search ? `${nextLocation.pathname}${nextLocation.search}` : nextLocation.pathname;
153
+ if (fullPath === window.location.pathname + window.location.search) return;
154
+ history.pushState(null, "", fullPath);
146
155
  }, [setLocation]);
147
156
  const transitionedNavigation = useCallback(({ nextLocation, isFirstCall, isAnimated }) => {
148
157
  if (isAnimated && !isFirstCall) try {
@@ -161,9 +170,10 @@ var useHandleNavigation = ({ setLocation, routeList, context, revalidateCache, i
161
170
  pathname: nextLocation.pathname
162
171
  });
163
172
  if (nextItem?.beforeLoad) try {
173
+ const redirect = async (location) => typeof location === "string" ? await navigationHandler({ pathname: location }) : await navigationHandler(location);
164
174
  await nextItem.beforeLoad({
165
175
  context,
166
- redirect: navigationHandler,
176
+ redirect,
167
177
  params
168
178
  });
169
179
  } catch {
@@ -264,7 +274,9 @@ var useLoader = ({ routeList, context }) => {
264
274
  const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
265
275
  if (!routeItem) return true;
266
276
  const currentCacheTimestamp = cacheTimestampsRef.current[pathname];
267
- return Boolean(currentCacheTimestamp && Date.now() - currentCacheTimestamp < (routeItem.staleTime || 0));
277
+ if (!currentCacheTimestamp) return false;
278
+ if (!routeItem.staleTime) return true;
279
+ return Date.now() - currentCacheTimestamp < routeItem.staleTime;
268
280
  }, []);
269
281
  const revalidateCache = useCallback(async ({ routeItem, isCurrentRoute, pathname }) => {
270
282
  if (!routeItem?.loader) return;
@@ -322,7 +334,7 @@ var useLoader = ({ routeList, context }) => {
322
334
  };
323
335
  //#endregion
324
336
  //#region hooks/useApplyCustomAnimation.ts
325
- var useApplyCustomAnimation = (animationOptions) => {
337
+ var useApplyCustomAnimation = (animationDuration) => {
326
338
  useEffect(() => {
327
339
  const style = document.createElement("style");
328
340
  style.id = "spinner-style";
@@ -349,51 +361,61 @@ var useApplyCustomAnimation = (animationOptions) => {
349
361
  return () => style.remove();
350
362
  }, []);
351
363
  useEffect(() => {
352
- if (!animationOptions?.duration) return;
364
+ if (!animationDuration) return;
353
365
  const style = document.createElement("style");
354
366
  style.id = "dynamic-view-transition-duration-style";
355
- style.textContent = `::view-transition-group(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
- }`;
367
+ style.textContent = `::view-transition-group(page) { animation-duration: ${animationDuration}ms; }`;
394
368
  document.head.appendChild(style);
395
369
  return () => style.remove();
396
- }, [animationOptions]);
370
+ }, [animationDuration]);
371
+ };
372
+ //#endregion
373
+ //#region components/RouterProvider.tsx
374
+ var RouterProvider = ({ children, routeList, context: initialContext = {}, isAnimated = false, animationDuration }) => {
375
+ const [location, setLocation] = useState({});
376
+ const [context, setContext] = useState(initialContext);
377
+ useApplyCustomAnimation(animationDuration);
378
+ const { loaderError, loaderCache, prefetchLoader, revalidateCache, isLoading } = useLoader({
379
+ routeList,
380
+ context
381
+ });
382
+ const { blockerState, updateLocation, updateBlockedRoute, beforeLoadError } = useHandleNavigation({
383
+ setLocation,
384
+ routeList,
385
+ context,
386
+ revalidateCache,
387
+ isAnimated
388
+ });
389
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
390
+ ...useMemo(() => ({
391
+ location,
392
+ updateLocation,
393
+ loaderCache,
394
+ prefetchLoader,
395
+ updateBlockedRoute,
396
+ blockerState,
397
+ context,
398
+ setContext,
399
+ routeList,
400
+ isLoading,
401
+ shouldErrorElementShown: loaderError || beforeLoadError,
402
+ isAnimated
403
+ }), [
404
+ beforeLoadError,
405
+ blockerState,
406
+ context,
407
+ isLoading,
408
+ loaderCache,
409
+ loaderError,
410
+ location,
411
+ prefetchLoader,
412
+ routeList,
413
+ updateBlockedRoute,
414
+ updateLocation,
415
+ isAnimated
416
+ ]),
417
+ children
418
+ });
397
419
  };
398
420
  //#endregion
399
421
  //#region components/Spinner.tsx
@@ -405,13 +427,34 @@ var renderElement = (Component) => {
405
427
  return typeof Component === "function" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, {}) : Component;
406
428
  };
407
429
  //#endregion
430
+ //#region hooks/useServiceContext.ts
431
+ var useServiceState = (reactContext) => {
432
+ const context = useContext(reactContext);
433
+ if (!Object.keys(context).length) throw new Error("hooks and Router component must be used within RouterProvider");
434
+ return context;
435
+ };
436
+ var useNavigationState = () => useServiceState(NavigationContext);
437
+ var useRouterActions = () => useServiceState(ActionsContext);
438
+ var useRouterData = () => useServiceState(DataContext);
439
+ var usePropsData = () => useServiceState(PropsContext);
440
+ //#endregion
441
+ //#region context/RouterViewContext.ts
442
+ var RouterViewContext = createContext({});
443
+ //#endregion
444
+ //#region provider/ViewProvider.tsx
445
+ var ViewProvider = ({ children, params }) => {
446
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterViewContext.Provider, {
447
+ value: params,
448
+ children
449
+ });
450
+ };
451
+ //#endregion
408
452
  //#region components/Router.tsx
409
453
  var PAGE_NOT_FOUND = "error 404. Page not found";
410
454
  var ALL_LOCATIONS = "*";
411
- var Router = ({ routeList, context: initialContext = {}, isAnimated = false, animationOptions = {} }) => {
412
- const [location, setLocation] = useState({});
413
- const [context, setContext] = useState(initialContext);
414
- useApplyCustomAnimation(animationOptions);
455
+ var Router = ({ spinner = true }) => {
456
+ const { location, isLoading, shouldErrorElementShown } = useNavigationState();
457
+ const { routeList, isAnimated } = usePropsData();
415
458
  const routeItem = useMemo(() => {
416
459
  if (!location.pathname) return void 0;
417
460
  return routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, location.pathname));
@@ -420,74 +463,41 @@ var Router = ({ routeList, context: initialContext = {}, isAnimated = false, ani
420
463
  routeItem,
421
464
  pathname: window.location.pathname
422
465
  }), [routeItem]);
423
- const { loaderError, loaderCache, prefetchLoader, revalidateCache, isLoading } = useLoader({
424
- routeList,
425
- context
426
- });
427
- const { blockerState, updateLocation, updateBlockedRoute, beforeLoadError } = useHandleNavigation({
428
- setLocation,
429
- routeList,
430
- context,
431
- revalidateCache,
432
- isAnimated
433
- });
434
- const providerProps = useMemo(() => ({
435
- location,
436
- updateLocation,
437
- params,
438
- loaderCache,
439
- prefetchLoader,
440
- updateBlockedRoute,
441
- blockerState,
442
- context,
443
- setContext
444
- }), [
445
- blockerState,
446
- loaderCache,
447
- location,
448
- params,
449
- prefetchLoader,
450
- context,
451
- updateBlockedRoute,
452
- updateLocation
453
- ]);
466
+ if (spinner && !routeItem && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
454
467
  if (!routeItem && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
455
468
  if (!routeItem) return null;
456
- if (!isAnimated && routeItem?.loader && !loaderError && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RouterProvider, {
457
- ...providerProps,
469
+ if (!isAnimated && routeItem?.loader && !shouldErrorElementShown && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewProvider, {
470
+ params,
458
471
  children: renderElement(routeItem?.loaderFallback)
459
472
  });
460
- if (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, {})]
473
+ if (shouldErrorElementShown) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ViewProvider, {
474
+ params,
475
+ children: [renderElement(routeItem?.errorElement), spinner && isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
463
476
  });
464
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RouterProvider, {
465
- ...providerProps,
466
- children: [renderElement(routeItem?.element) || PAGE_NOT_FOUND, isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
477
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
478
+ style: { viewTransitionName: "page" },
479
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ViewProvider, {
480
+ params,
481
+ children: [renderElement(routeItem?.element) || PAGE_NOT_FOUND, spinner && isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
482
+ })
467
483
  });
468
484
  };
469
485
  //#endregion
470
- //#region hooks/useServiceContext.ts
471
- var useNavigationState = () => {
472
- const context = useContext(NavigationContext);
473
- if (!Object.keys(context).length) throw new Error("useNavigationState must be used within Router component");
474
- return context;
475
- };
476
- var useRouterActions = () => {
477
- const context = useContext(ActionsContext);
478
- if (!Object.keys(context).length) throw new Error("useRouterActions must be used within Router component");
479
- return context;
480
- };
481
- var useRouterData = () => {
482
- const context = useContext(DataContext);
483
- if (!Object.keys(context).length) throw new Error("useRouterData must be used within Router component");
484
- return context;
486
+ //#region hooks/useLocation.ts
487
+ var useLocation = () => {
488
+ return useNavigationState().location;
485
489
  };
486
490
  //#endregion
487
491
  //#region hooks/useNavigate.ts
488
492
  var useNavigate = () => {
489
493
  const { updateLocation } = useRouterActions();
490
- return useCallback(async (arg) => arg === -1 ? history.go(-1) : await updateLocation(arg), [updateLocation]);
494
+ const locationRef = useLatest(useLocation());
495
+ return useCallback(async (arg) => {
496
+ if (typeof arg === "number") return history.go(arg);
497
+ if (typeof arg === "string") {
498
+ if (arg !== locationRef.current.pathname) await updateLocation({ pathname: arg });
499
+ } else if (JSON.stringify(arg) !== JSON.stringify(locationRef.current)) await updateLocation(arg);
500
+ }, [updateLocation, locationRef]);
491
501
  };
492
502
  //#endregion
493
503
  //#region components/Link.tsx
@@ -496,7 +506,7 @@ var Link = ({ children, to, prefetch = true }) => {
496
506
  const navigate = useNavigate();
497
507
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
498
508
  style: { cursor: "pointer" },
499
- onClick: () => navigate({ pathname: to }),
509
+ onClick: () => navigate(to),
500
510
  onMouseOver: () => prefetch && prefetchLoader(to),
501
511
  children
502
512
  });
@@ -504,13 +514,7 @@ var Link = ({ children, to, prefetch = true }) => {
504
514
  //#endregion
505
515
  //#region hooks/useParams.ts
506
516
  var useParams = () => {
507
- const { params } = useNavigationState();
508
- return params;
509
- };
510
- //#endregion
511
- //#region hooks/useLocation.ts
512
- var useLocation = () => {
513
- return useNavigationState().location;
517
+ return useContext(RouterViewContext);
514
518
  };
515
519
  //#endregion
516
520
  //#region hooks/useLoaderState.ts
@@ -562,4 +566,38 @@ var useRouterContext = () => {
562
566
  };
563
567
  };
564
568
  //#endregion
565
- export { Link, Router, createRouter, useBeforeUnload, useBlocker, useLoaderState, useLocation, useNavigate, useParams, useRouterContext };
569
+ //#region hooks/useSearchParams.ts
570
+ var useSearchParams = () => {
571
+ const location = useLocation();
572
+ const navigate = useNavigate();
573
+ const locationRef = useLatest(location);
574
+ const searchString = useMemo(() => location.search ? location.search.replace("?", "") : location.pathname.split("?")?.[1] ?? "", [location.pathname, location.search]);
575
+ const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
576
+ const getSearchParams = useCallback((param) => {
577
+ const allValues = searchParams.getAll(param);
578
+ return allValues.length > 1 ? allValues : allValues[0] ?? "";
579
+ }, [searchParams]);
580
+ const navigateWithSearchParams = useCallback((params) => {
581
+ const newSearch = params.toString();
582
+ navigate({
583
+ pathname: locationRef.current.pathname,
584
+ search: newSearch ? `?${newSearch}` : ""
585
+ });
586
+ }, [locationRef, navigate]);
587
+ return {
588
+ searchParams,
589
+ getSearchParams,
590
+ setSearchParams: useCallback((param, value) => {
591
+ const search = locationRef.current.search || "";
592
+ const currentParams = new URLSearchParams(search);
593
+ if (typeof param === "string" && value !== void 0) {
594
+ currentParams.delete(param);
595
+ (Array.isArray(value) ? value : [value]).forEach((v) => currentParams.append(param, v));
596
+ navigateWithSearchParams(currentParams);
597
+ } else if (typeof param === "function") navigateWithSearchParams(param(currentParams));
598
+ else throw new Error("useSearchParams first argument must be either function or string");
599
+ }, [locationRef, navigateWithSearchParams])
600
+ };
601
+ };
602
+ //#endregion
603
+ export { Link, Router, RouterProvider, createRouter, useBeforeUnload, useBlocker, useLoaderState, useLocation, useNavigate, useParams, useRouterContext, useSearchParams };
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type ActionsContextValue, type DataContextValue, type NavigationContextValue, PropsContextValue } from '../context/RouterProviderContext';
3
+ type ProviderProps = PropsContextValue & NavigationContextValue & ActionsContextValue & DataContextValue & {
4
+ children: ReactNode;
5
+ };
6
+ export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation, location, prefetchLoader, loaderCache, blockerState, routeList, shouldErrorElementShown, isLoading, isAnimated, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -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.1",
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 {};