clear-react-router 1.6.2 → 1.6.4

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
@@ -2,21 +2,33 @@
2
2
 
3
3
  A lightweight, type-safe routing library for React applications with nested routes, data loading, navigation blocking, and prefetching.
4
4
 
5
+ ## Why Clear Router?
6
+
7
+ Most React routers focus on flexibility and ecosystem integrations.
8
+ Clear Router focuses on predictable navigation with a small, explicit API.
9
+
10
+ It provides first-class support for:
11
+
12
+ - Predictable routing
13
+ - Built-in data loading
14
+ - Small API
15
+
5
16
  ## Features
6
17
 
7
- - 🧩 **Nested Routes** - Organize your UI with nested layouts and routes
8
- - **Data Loading** - Built-in loaders with caching and stale-while-revalidate strategy
9
- - 🔒 **Navigation Blocking** - Prevent accidental navigation with `useBlocker`
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
12
- - 🎯 **Type-safe Redirects** - Redirect from beforeLoad hook
13
- - 📦 **Prefetching** - Preload data on hover for instant navigation
14
- - 🚀 **Lazy Loading** - Code-split your routes with dynamic imports for optimal performance
15
- - 📍 **Scroll Restoration** Automatically saves and restores scroll position when navigating back to a page (preserves user's scroll position)
16
- - 🔍 **Typed Query Param** — Type-safe reading and writing of URL query parameters with built-in parsers for strings, numbers, booleans, arrays, and Zod schemas
17
- - 🎨 **Flexible API** - Use components or hooks as you prefer
18
- - 📱 **Browser History** - Full support for browser back/forward buttons
19
- - 🧠 **Context-aware** - Pass and update context through routes
18
+ - **Nested Routes** - Organize your UI with nested layouts and routes
19
+ - **Data Loading** - Built-in loaders with caching and stale-while-revalidate strategy
20
+ - **Navigation Blocking** - Prevent accidental navigation with `useBlocker`
21
+ - **Smooth Animations** - Page transitions with fade effect (customizable duration)
22
+ - **Static Layout** — Keep navbar, footer, and other elements outside the router to avoid unnecessary re-renders
23
+ - **Programmatic Redirects** - Redirect from beforeLoad hook
24
+ - **Cache invalidation** - Manual route invalidation
25
+ - **Prefetching** - Preload data on hover for instant navigation
26
+ - **Lazy Loading** - Code-split your routes with dynamic imports for optimal performance
27
+ - **Scroll Restoration** — Automatically saves and restores scroll position when navigating back to a page (preserves user's scroll position)
28
+ - **Typed Query Param** — Type-safe reading and writing of URL query parameters with built-in parsers for strings, numbers, booleans, arrays, and Zod schemas
29
+ - **Flexible API** - Use components or hooks as you prefer
30
+ - **Browser History** - Full support for browser back/forward buttons
31
+ - **Context-aware** - Pass and update context through routes
20
32
 
21
33
  ## API
22
34
 
@@ -28,8 +40,8 @@ Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic pa
28
40
  |----------|------|-------------|
29
41
  | `path` | `string` | Route path, e.g., `/user/:userId` |
30
42
  | `element` | `ReactElement \| () => ReactElement \| LazyComponent` | Component to render |
31
- | `loader` | `({ params, context, setContext }) => Promise<unknown>` | Fetch data using route params and context. Can update context via `setContext` |
32
43
  | `beforeLoad` | `({ params, context, redirect, setContext }) => Promise<unknown> \| undefined \| void` | Auth checks and redirects. Can update context via `setContext`. `redirect` is provided by the router |
44
+ | `loader` | `({ params, context, setContext }) => Promise<unknown>` | Fetch data using route params and context. Can update context via `setContext` |
33
45
  | `afterLoad` | `({ params, context, setContext }) => Promise<void>` | Analytics, side effects after data is loaded. Can update context via `setContext` |
34
46
  | `fallback` | `ReactElement \| () => ReactElement` | Loading fallback (for lazy loading) |
35
47
  | `loaderFallback` | `ReactElement \| () => ReactElement` | Loading fallback (for loader) |
@@ -43,7 +55,7 @@ The root component that provides routing context to the application. Place stati
43
55
 
44
56
  | Prop | Type | Default | Description |
45
57
  |------|------|---------|-------------|
46
- | `routeList` | `RouteItem[]` | required | Array of route configurations |
58
+ | `routes` | `RouteItem[]` | required | Array of route configurations |
47
59
  | `context` | `object` | `{}` | Initial context (user, theme, etc.) |
48
60
  | `children` | `ReactNode` | required | App content (must include `<Router />`) |
49
61
 
@@ -71,12 +83,13 @@ Renders the current route's component. Must be placed inside `<RouterProvider>`.
71
83
  | `animationDuration` | `number` | `optional` | Animation duration in milliseconds (browser default is used if not set) |
72
84
  | `spinner` | `boolean \| undefined` | `true` | Show a small spinner in the corner while loading data (only when `isAnimated` is enabled) |
73
85
  | `preserveScroll` | `boolean \| undefined` | `true` | Save and restore scroll position when navigating between pages |
74
- | `showFallbackIfAnimated` | `boolean \| undefined` | `false` | Show `loaderFallback` even when `isAnimated` is `true` (instead of spinner) |
86
+ | `showFallbackOnAnimation` | `boolean \| undefined` | `false` | Show `loaderFallback` even when `isAnimated` is `true` (instead of spinner) |
75
87
  | `prefetch` | `'hover' \| 'render' \| 'viewport' \| 'none'` | `'hover'` | Default prefetch strategy for all `<Link>` components |
76
88
  | `hoverPrefetchDelay` | `number` | `150` | Delay in milliseconds before prefetching on hover (only for `'hover'` strategy) |
89
+ | `errorBoundary` | `ComponentType<{ children: ReactNode }>` | `undefined` | Custom error boundary component for catching render errors in route components |
77
90
 
78
91
  ```
79
- <RouterProvider routeList={routes}>
92
+ <RouterProvider routes={routes}>
80
93
  <Navbar />
81
94
  <Router spinner={false} isAnimated /> {/* disable the spinner */}
82
95
  </RouterProvider>
@@ -110,7 +123,7 @@ Component for client-side navigation with prefetch support.
110
123
  import { RouterProvider, Router, Link } from 'clear-react-router';
111
124
 
112
125
  // Global prefetch: hover with 100ms delay
113
- <RouterProvider routeList={routes} prefetch="hover" hoverPrefetchDelay={100}>
126
+ <RouterProvider routes={routes} prefetch="hover" hoverPrefetchDelay={100}>
114
127
  <Router />
115
128
  </RouterProvider>
116
129
 
@@ -204,6 +217,23 @@ const routes = createRouter([
204
217
  ]);
205
218
  ```
206
219
 
220
+ ### Error Boundaries
221
+
222
+ You can provide a custom error boundary to catch rendering errors in route components. This is useful for preventing the entire app from crashing when a specific route fails to render.
223
+
224
+ ```tsx
225
+ import { RouterProvider, Router } from 'clear-react-router';
226
+ import { routes } from './routes';
227
+ import { ErrorBoundary } from './components/ErrorBoundary';
228
+
229
+ const App = () => (
230
+ <RouterProvider routes={routes}>
231
+ <Router errorBoundary={ErrorBoundary} />
232
+ </RouterProvider>
233
+ );
234
+ ```
235
+ **Note:** The `errorBoundary` prop only catches render-time errors in route components. It does not catch errors in `loader` or `beforeLoad` — those are handled by the router's `errorElement` mechanism.
236
+
207
237
  ## Hooks
208
238
 
209
239
  ### `useNavigate()`
@@ -245,21 +275,21 @@ Returns current location `{ pathname, search, state }`.
245
275
  const { pathname, search, state } = useLocation();
246
276
  ```
247
277
 
248
- ### `useLoaderState()`
278
+ ### `useLoaderState<T>()`
249
279
 
250
280
  Returns the cached data loaded by the current route's `loader`, along with any errors from `loader` or `beforeLoad`. Data is automatically cached and reused when navigating back to the same route.
251
281
 
252
282
  **Returns:**
253
283
 
254
284
  | Property | Type | Description |
255
- |----------|------|-------------|
256
- | `data` | `unknown` | The data returned from the route's `loader` |
285
+ |----------|:----:|-------------|
286
+ | `data` | `T` | The data returned from the route's `loader` |
257
287
  | `loaderError` | `Error \| null` | Error from the `loader` (if any) |
258
288
  | `beforeLoadError` | `Error \| null` | Error from the `beforeLoad` hook (if any) |
259
289
 
260
290
  ```
261
- function UserProfile() {
262
- const { data, loaderError, beforeLoadError } = useLoaderState();
291
+ const UserProfile = () => {
292
+ const { data, loaderError, beforeLoadError } = useLoaderState<User>();
263
293
  ```
264
294
 
265
295
  ### Caching behavior:
@@ -273,6 +303,71 @@ function UserProfile() {
273
303
  }
274
304
  ```
275
305
 
306
+ ### `useInvalidate()`
307
+
308
+ Returns a function that marks route data as stale and re-runs the route lifecycle.
309
+
310
+ Calling `invalidate()` clears the cached loader result for a route and executes both `beforeLoad` and `loader` again. This is useful after mutations or any operation that changes data used by the route.
311
+
312
+ #### Current route
313
+
314
+ Invalidate the currently active route:
315
+
316
+ ```tsx
317
+ const invalidate = useInvalidate();
318
+
319
+ await invalidate();
320
+ ```
321
+
322
+ #### Specific route
323
+
324
+ You can also invalidate any registered route by passing its pathname:
325
+
326
+ ```tsx
327
+ const invalidate = useInvalidate();
328
+
329
+ await invalidate('/posts');
330
+ ```
331
+
332
+ The route does not need to be currently active. Its cache will be marked as stale, and the next time it is visited, `beforeLoad` and `loader` will run again.
333
+
334
+ #### Why use it?
335
+
336
+ A common use case is refreshing route data after a mutation.
337
+
338
+ For example, after deleting a post while viewing `/posts/42`, you may want the posts list to be reloaded the next time the user navigates to `/posts`:
339
+
340
+ ```tsx
341
+ const invalidate = useInvalidate();
342
+
343
+ await deletePost(id);
344
+ await invalidate('/posts');
345
+ ```
346
+
347
+ Likewise, after updating the current page, you can immediately refresh its data:
348
+
349
+ ```tsx
350
+ const invalidate = useInvalidate();
351
+
352
+ await updateProfile(data);
353
+ await invalidate();
354
+ ```
355
+
356
+ #### Notes
357
+
358
+ * `invalidate()` re-executes both `beforeLoad` and `loader` for the invalidated route.
359
+ * Cached data is discarded before the new loader starts.
360
+ * When used as an event handler, wrap the call in an arrow function:
361
+
362
+ ```tsx
363
+ <button onClick={() => invalidate()}>
364
+ Refresh
365
+ </button>
366
+ ```
367
+
368
+ Passing `invalidate` directly (`onClick={invalidate}`) is not supported because React passes a `MouseEvent` object to event handlers.
369
+
370
+
276
371
  ### `useBlocker(callback)`
277
372
 
278
373
  Blocks navigation when callback returns `true`.
@@ -370,7 +465,7 @@ type Adapter<T> = {
370
465
  | Element | Type | Description |
371
466
  |---------|------|-------------|
372
467
  | `value` | `T` | The parsed value from the query parameter |
373
- | `setValue` | `(arg: T | null) => void` | Function to update the query parameter. Null is passed to remove the parameter. |
468
+ | `setValue` | `(arg: T \| null) => void` | Function to update the query parameter. Null is passed to remove the parameter. |
374
469
 
375
470
  ### Built-in Adapters
376
471
 
@@ -489,7 +584,6 @@ function ProductFilter() {
489
584
 
490
585
  - **Array support** — `getSearchParams` returns `string[]` when multiple values exist for the same key
491
586
  - **Functional updates** — Update parameters based on previous state without losing other params
492
- - **Type-safe** — Proper TypeScript support with overloads
493
587
  - **Stable reference** — `setSearchParams` reference is stable and safe to use in `useEffect`
494
588
 
495
589
  > **Note:** `getSearchParams` returns `string` for single values, `string[]` for multiple values, and `''` if the key is not found.
@@ -1,2 +1,2 @@
1
1
  import { RouterProps } from '../types/global';
2
- export declare const Router: ({ isAnimated, animationDuration, spinner, preserveScroll, showFallbackIfAnimated, prefetch, hoverPrefetchDelay, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
2
+ export declare const Router: ({ isAnimated, animationDuration, spinner, preserveScroll, showFallbackOnAnimation, prefetch, hoverPrefetchDelay, errorBoundary: ErrorBoundary, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -2,8 +2,8 @@ import { ReactNode } from 'react';
2
2
  import { RouteItem } from '../types/global';
3
3
  type RouteProviderProps = {
4
4
  children: ReactNode;
5
- routeList: RouteItem[];
5
+ routes: RouteItem[];
6
6
  context?: Record<string, unknown>;
7
7
  };
8
- export declare const RouterProvider: ({ children, routeList, context: initialContext }: RouteProviderProps) => import("react/jsx-runtime").JSX.Element;
8
+ export declare const RouterProvider: ({ children, routes, context: initialContext }: RouteProviderProps) => import("react/jsx-runtime").JSX.Element;
9
9
  export {};
@@ -1,7 +1,7 @@
1
1
  import { RouterProps } from '../types/global';
2
2
  declare class RouterConfig {
3
3
  isAnimated: boolean;
4
- showFallbackIfAnimated: boolean;
4
+ showFallbackOnAnimation: boolean;
5
5
  prefetch: RouterProps['prefetch'];
6
6
  hoverPrefetchDelay: number;
7
7
  configure(config: Partial<RouterConfig>): void;
@@ -12,6 +12,7 @@ export type ActionsContextValue = {
12
12
  prefetchLoader(arg: string): Promise<void>;
13
13
  setContext(arg: object): void;
14
14
  restoreScroll(): void;
15
+ invalidate(path?: string): void;
15
16
  };
16
17
  export type DataContextValue = {
17
18
  context: Record<string, unknown>;
@@ -1,7 +1,7 @@
1
1
  import { type Dispatch, RefObject, type SetStateAction } from 'react';
2
2
  import { BlockerState, LoaderState, Location, RevalidateCacheArgs, RouteItem, RouteItemData, UpdateBlockedRouteProps } from '../types/global';
3
3
  type UseHandleNavigation = {
4
- routeList: RouteItem[];
4
+ routes: RouteItem[];
5
5
  context: Record<string, unknown>;
6
6
  revalidateCache(arg: RevalidateCacheArgs): Promise<unknown>;
7
7
  setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
@@ -10,15 +10,17 @@ type UseHandleNavigation = {
10
10
  pathname: string;
11
11
  }): boolean;
12
12
  loaderStateRef: RefObject<LoaderState>;
13
+ clearTimestamp(path: string): void;
13
14
  };
14
- export declare const useHandleNavigation: ({ routeList, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef, }: UseHandleNavigation) => {
15
+ export declare const useHandleNavigation: ({ routes, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef, clearTimestamp, }: UseHandleNavigation) => {
15
16
  blockerState: BlockerState;
16
17
  updateLocation: (nextLocation: Location) => Promise<void>;
17
18
  updateBlockedRoute: ({ type, payload }: UpdateBlockedRouteProps) => void;
18
19
  routeItemData: RouteItemData;
19
20
  restoreScroll: () => void;
20
- currentLoaderFallback: import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | (() => import("react").ReactElement) | undefined;
21
+ currentLoaderFallback: (import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | (() => import("react").ReactElement)) | undefined;
21
22
  isLoading: boolean;
22
23
  loaderState: LoaderState;
24
+ invalidate: (pathname?: string) => Promise<void>;
23
25
  };
24
26
  export {};
@@ -0,0 +1 @@
1
+ export declare const useInvalidate: () => (path?: string) => void;
@@ -1,11 +1,11 @@
1
1
  import { type Dispatch, type SetStateAction } from 'react';
2
2
  import type { LoaderState, RevalidateCacheArgs, RouteItem } from '../types/global';
3
3
  type UseLoaderParams = {
4
- routeList: RouteItem[];
4
+ routes: RouteItem[];
5
5
  context: Record<string, unknown>;
6
6
  setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
7
7
  };
8
- export declare const useLoader: ({ routeList, context, setContext }: UseLoaderParams) => {
8
+ export declare const useLoader: ({ routes, context, setContext }: UseLoaderParams) => {
9
9
  prefetchLoader: (pathname: string) => Promise<void>;
10
10
  revalidateCache: ({ routeItem, pathname }: RevalidateCacheArgs) => Promise<unknown>;
11
11
  isCacheItemFresh: ({ routeItem, pathname }: {
@@ -13,5 +13,6 @@ export declare const useLoader: ({ routeList, context, setContext }: UseLoaderPa
13
13
  pathname: string;
14
14
  }) => boolean;
15
15
  loaderStateRef: import("react").RefObject<LoaderState>;
16
+ clearTimestamp: (pathname: string) => void;
16
17
  };
17
18
  export {};
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { useNavigate } from './hooks/useNavigate';
5
5
  export { useParams } from './hooks/useParams';
6
6
  export { useLocation } from './hooks/useLocation';
7
7
  export { useLoaderState } from './hooks/useLoaderState';
8
+ export { useInvalidate } from './hooks/useInvalidate';
8
9
  export { useBlocker } from './hooks/useBlocker';
9
10
  export { useBeforeUnload } from './hooks/useBeforeUnload';
10
11
  export { useRouterContext } from './hooks/useRouterContext';
@@ -13,4 +14,4 @@ export { useSearchParams } from './hooks/useSearchParams';
13
14
  export { useHistoricalTrail } from './hooks/useHistoricalTrail';
14
15
  export { adapter } from './utils/adapter';
15
16
  export { createRouter } from './utils/utils';
16
- export type { RouteItem, BlockerState, Location, AdapterType } from './types/global';
17
+ export type { RouteItem, BlockerState, Location, AdapterType, RouterProps } from './types/global';
package/dist/index.js CHANGED
@@ -45,14 +45,15 @@ var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((expor
45
45
  var import_jsx_runtime = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
46
46
  module.exports = require_react_jsx_runtime_production();
47
47
  })))();
48
- var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocation, prefetchLoader, blockerState, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState }) => {
48
+ var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocation, prefetchLoader, blockerState, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState, invalidate }) => {
49
49
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionsContext.Provider, {
50
50
  value: {
51
51
  updateLocation,
52
52
  updateBlockedRoute,
53
53
  prefetchLoader,
54
54
  setContext,
55
- restoreScroll
55
+ restoreScroll,
56
+ invalidate
56
57
  },
57
58
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DataContext.Provider, {
58
59
  value: { context },
@@ -124,7 +125,7 @@ function _defineProperty(e, r, t) {
124
125
  var RouterConfig = class {
125
126
  constructor() {
126
127
  _defineProperty(this, "isAnimated", false);
127
- _defineProperty(this, "showFallbackIfAnimated", false);
128
+ _defineProperty(this, "showFallbackOnAnimation", false);
128
129
  _defineProperty(this, "prefetch", "hover");
129
130
  _defineProperty(this, "hoverPrefetchDelay", 150);
130
131
  }
@@ -187,8 +188,8 @@ var comparePaths = (el, pathname) => {
187
188
  //#endregion
188
189
  //#region hooks/useHandleNavigation.ts
189
190
  var ALL_LOCATIONS = "*";
190
- var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef }) => {
191
- const { isAnimated, showFallbackIfAnimated: showFallback } = routerConfig;
191
+ var useHandleNavigation = ({ routes, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef, clearTimestamp }) => {
192
+ const { isAnimated, showFallbackOnAnimation: showFallback } = routerConfig;
192
193
  const [isLoading, setIsLoading] = useState(false);
193
194
  const [blockedRoute, setBlockedRoute] = useState({
194
195
  from: "",
@@ -203,16 +204,16 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
203
204
  const [loaderState, setLoaderState] = useState(emptyLoaderState);
204
205
  const prevPathname = useRef("");
205
206
  const navigationSeq = useRef(0);
206
- const scrollMapRef = useLatest(scrollMap);
207
+ const scrollMapLatest = useLatest(scrollMap);
207
208
  const restoreScroll = useCallback(() => {
208
- if (!prevPathname.current || !scrollMapRef.current[prevPathname.current]) return;
209
+ if (!prevPathname.current || !scrollMapLatest.current[prevPathname.current]) return;
209
210
  requestAnimationFrame(() => {
210
211
  window.scrollTo({
211
- top: scrollMapRef.current[prevPathname.current],
212
+ top: scrollMapLatest.current[prevPathname.current],
212
213
  behavior: "smooth"
213
214
  });
214
215
  });
215
- }, [scrollMapRef]);
216
+ }, [scrollMapLatest]);
216
217
  const navigation = useCallback((nextLocation, routeItem) => {
217
218
  setRouteItemData({
218
219
  routeItem,
@@ -237,21 +238,19 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
237
238
  navigation(nextLocation, routeItem);
238
239
  }
239
240
  }, [navigation, isAnimated]);
240
- const navigationHandler = useCallback(async (nextLocation) => {
241
- navigationSeq.current = navigationSeq.current + 1;
242
- const seq = navigationSeq.current;
243
- loaderStateRef.current = emptyLoaderState;
244
- const nextItem = routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, nextLocation.pathname));
245
- const params = getParamsObject({
246
- params: nextItem?.params,
247
- pathname: nextLocation.pathname
241
+ const invalidate = useCallback(async (pathname = routeItemData.location.pathname) => {
242
+ if (typeof pathname !== "string") return;
243
+ const routeItem = routes.find((el) => comparePaths(el, pathname));
244
+ const resultParams = getParamsObject({
245
+ params: routeItem?.params,
246
+ pathname
248
247
  });
249
- if (nextItem?.beforeLoad) try {
250
- const redirect = async (location) => await navigationHandler(typeof location === "string" ? { pathname: location } : location);
251
- await nextItem.beforeLoad({
248
+ clearTimestamp(pathname);
249
+ try {
250
+ if (routeItem?.beforeLoad) await routeItem.beforeLoad({
252
251
  context,
253
- redirect,
254
- params,
252
+ redirect: () => Promise.resolve(),
253
+ params: resultParams,
255
254
  setContext
256
255
  });
257
256
  loaderStateRef.current = {
@@ -263,7 +262,50 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
263
262
  ...loaderStateRef.current,
264
263
  beforeLoadError: error
265
264
  };
266
- return transitionedNavigation(nextLocation, nextItem);
265
+ }
266
+ await revalidateCache({
267
+ routeItem,
268
+ pathname
269
+ });
270
+ if (pathname === routeItemData.location.pathname) setLoaderState(loaderStateRef.current);
271
+ }, [
272
+ clearTimestamp,
273
+ context,
274
+ loaderStateRef,
275
+ revalidateCache,
276
+ routeItemData.location.pathname,
277
+ routes,
278
+ setContext
279
+ ]);
280
+ const navigationHandler = useCallback(async (nextLocation) => {
281
+ navigationSeq.current = navigationSeq.current + 1;
282
+ const seq = navigationSeq.current;
283
+ loaderStateRef.current = emptyLoaderState;
284
+ const nextItem = routes.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, nextLocation.pathname));
285
+ const params = getParamsObject({
286
+ params: nextItem?.params,
287
+ pathname: nextLocation.pathname
288
+ });
289
+ if (nextItem?.beforeLoad) {
290
+ const redirect = async (location) => await navigationHandler(typeof location === "string" ? { pathname: location } : location);
291
+ try {
292
+ await nextItem.beforeLoad({
293
+ context,
294
+ redirect,
295
+ params,
296
+ setContext
297
+ });
298
+ loaderStateRef.current = {
299
+ ...loaderStateRef.current,
300
+ beforeLoadError: null
301
+ };
302
+ } catch (error) {
303
+ loaderStateRef.current = {
304
+ ...loaderStateRef.current,
305
+ beforeLoadError: error
306
+ };
307
+ return transitionedNavigation(nextLocation, nextItem);
308
+ }
267
309
  }
268
310
  if (seq !== navigationSeq.current) return;
269
311
  setScrollMap((prevState) => {
@@ -295,7 +337,7 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
295
337
  }, [
296
338
  context,
297
339
  revalidateCache,
298
- routeList,
340
+ routes,
299
341
  transitionedNavigation,
300
342
  setContext,
301
343
  isCacheItemFresh,
@@ -359,19 +401,20 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
359
401
  restoreScroll,
360
402
  currentLoaderFallback,
361
403
  isLoading,
362
- loaderState
404
+ loaderState,
405
+ invalidate
363
406
  };
364
407
  };
365
408
  //#endregion
366
409
  //#region hooks/useLoader.ts
367
- var useLoader = ({ routeList, context, setContext }) => {
368
- const timestampMapRef = useRef({});
410
+ var useLoader = ({ routes, context, setContext }) => {
411
+ const timestampMapRef = useRef(/* @__PURE__ */ new Map());
369
412
  const loaderMapRef = useRef({});
370
413
  const loaderStateRef = useRef(emptyLoaderState);
371
414
  const loadingPromises = useRef(/* @__PURE__ */ new Map());
372
415
  const isCacheItemFresh = useCallback(({ routeItem, pathname }) => {
373
416
  if (!routeItem) return true;
374
- const currentCacheTimestamp = timestampMapRef.current[pathname];
417
+ const currentCacheTimestamp = timestampMapRef.current.get(pathname);
375
418
  if (!currentCacheTimestamp) return false;
376
419
  if (!routeItem.staleTime) return true;
377
420
  return Date.now() - currentCacheTimestamp < routeItem.staleTime;
@@ -398,20 +441,13 @@ var useLoader = ({ routeList, context, setContext }) => {
398
441
  context,
399
442
  setContext
400
443
  });
401
- timestampMapRef.current = {
402
- ...timestampMapRef.current,
403
- [pathname]: Date.now()
404
- };
405
- loaderMapRef.current[pathname] = {
406
- data: result,
407
- loaderError: null,
408
- beforeLoadError: null
409
- };
444
+ timestampMapRef.current.set(pathname, Date.now());
410
445
  loaderStateRef.current = {
411
446
  ...loaderStateRef?.current,
412
447
  data: result,
413
448
  loaderError: null
414
449
  };
450
+ loaderMapRef.current[pathname] = loaderStateRef.current;
415
451
  } catch (error) {
416
452
  loaderStateRef.current = {
417
453
  ...loaderStateRef?.current,
@@ -431,33 +467,37 @@ var useLoader = ({ routeList, context, setContext }) => {
431
467
  ]);
432
468
  return {
433
469
  prefetchLoader: useCallback(async (pathname) => {
434
- const item = routeList.find((el) => comparePaths(el, pathname));
470
+ const item = routes.find((el) => comparePaths(el, pathname));
435
471
  if (item) await revalidateCache({
436
472
  routeItem: item,
437
473
  pathname
438
474
  });
439
- }, [revalidateCache, routeList]),
475
+ }, [revalidateCache, routes]),
440
476
  revalidateCache,
441
477
  isCacheItemFresh,
442
- loaderStateRef
478
+ loaderStateRef,
479
+ clearTimestamp: useCallback((pathname) => {
480
+ timestampMapRef.current.delete(pathname);
481
+ }, [])
443
482
  };
444
483
  };
445
484
  //#endregion
446
485
  //#region components/RouterProvider.tsx
447
- var RouterProvider = ({ children, routeList, context: initialContext = {} }) => {
486
+ var RouterProvider = ({ children, routes, context: initialContext = {} }) => {
448
487
  const [context, setContext] = useState(initialContext);
449
- const { prefetchLoader, revalidateCache, isCacheItemFresh, loaderStateRef } = useLoader({
450
- routeList,
488
+ const { prefetchLoader, revalidateCache, isCacheItemFresh, loaderStateRef, clearTimestamp } = useLoader({
489
+ routes,
451
490
  context,
452
491
  setContext
453
492
  });
454
- const { blockerState, updateLocation, updateBlockedRoute, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState } = useHandleNavigation({
455
- routeList,
493
+ const { blockerState, updateLocation, updateBlockedRoute, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState, invalidate } = useHandleNavigation({
494
+ routes,
456
495
  context,
457
496
  setContext,
458
497
  revalidateCache,
459
498
  isCacheItemFresh,
460
- loaderStateRef
499
+ loaderStateRef,
500
+ clearTimestamp
461
501
  });
462
502
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
463
503
  ...useMemo(() => ({
@@ -471,7 +511,8 @@ var RouterProvider = ({ children, routeList, context: initialContext = {} }) =>
471
511
  restoreScroll,
472
512
  currentLoaderFallback,
473
513
  isLoading,
474
- loaderState
514
+ loaderState,
515
+ invalidate
475
516
  }), [
476
517
  blockerState,
477
518
  context,
@@ -482,7 +523,8 @@ var RouterProvider = ({ children, routeList, context: initialContext = {} }) =>
482
523
  currentLoaderFallback,
483
524
  restoreScroll,
484
525
  isLoading,
485
- loaderState
526
+ loaderState,
527
+ invalidate
486
528
  ]),
487
529
  children
488
530
  });
@@ -548,6 +590,11 @@ var usePreserveScroll = (preserveScroll) => {
548
590
  ]);
549
591
  };
550
592
  //#endregion
593
+ //#region hooks/useSetRouterConfig.ts
594
+ var useSetRouterConfig = (routerProps) => {
595
+ useEffect(() => routerConfig.configure(routerProps), [routerProps]);
596
+ };
597
+ //#endregion
551
598
  //#region components/Spinner.tsx
552
599
  var Spinner = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cr-spinner" });
553
600
  //#endregion
@@ -557,32 +604,28 @@ var renderElement = (Component) => {
557
604
  return typeof Component === "function" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, {}) : Component;
558
605
  };
559
606
  //#endregion
560
- //#region hooks/useSetRouterConfig.ts
561
- var useSetRouterConfig = (routerProps) => {
562
- useEffect(() => routerConfig.configure(routerProps), [routerProps]);
563
- };
564
- //#endregion
565
607
  //#region components/Router.tsx
566
- var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true, showFallbackIfAnimated = false, prefetch = "hover", hoverPrefetchDelay = 150 }) => {
567
- const { routeItemData: { routeItem }, loaderState, currentLoaderFallback, isLoading } = useNavigationState();
608
+ var EmptyBoundary = ({ children }) => children;
609
+ var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true, showFallbackOnAnimation = false, prefetch = "hover", hoverPrefetchDelay = 150, errorBoundary: ErrorBoundary = EmptyBoundary }) => {
610
+ const { routeItemData: { routeItem, location }, loaderState, currentLoaderFallback, isLoading } = useNavigationState();
568
611
  usePreserveScroll(preserveScroll);
569
612
  useApplyCustomAnimation(animationDuration);
570
613
  useSetRouterConfig({
571
614
  isAnimated,
572
- showFallbackIfAnimated,
615
+ showFallbackOnAnimation,
573
616
  prefetch,
574
617
  hoverPrefetchDelay
575
618
  });
576
619
  const showErrorElement = !isLoading && Boolean(loaderState.loaderError || loaderState.beforeLoadError);
577
620
  const showSpinner = spinner && isAnimated && isLoading;
578
621
  const loadingContent = !showErrorElement && isLoading;
579
- if ((showFallbackIfAnimated || !isAnimated) && loadingContent) return renderElement(currentLoaderFallback);
580
- if (!showFallbackIfAnimated && isAnimated && loadingContent) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
622
+ if ((showFallbackOnAnimation || !isAnimated) && loadingContent) return renderElement(currentLoaderFallback);
623
+ if (!showFallbackOnAnimation && isAnimated && loadingContent) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
581
624
  if (!routeItem) return null;
582
625
  if (showErrorElement) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [renderElement(routeItem.errorElement), showSpinner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})] });
583
626
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
584
627
  style: { viewTransitionName: "page" },
585
- children: [renderElement(routeItem.element) || null, showSpinner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
628
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorBoundary, { children: renderElement(routeItem.element) }, location.pathname), showSpinner && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
586
629
  });
587
630
  };
588
631
  //#endregion
@@ -668,11 +711,11 @@ var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }) => {
668
711
  //#endregion
669
712
  //#region hooks/useParams.ts
670
713
  var useParams = () => {
671
- const { routeItemData: { routeItem } } = useNavigationState();
714
+ const { routeItemData: { routeItem, location: { pathname } } } = useNavigationState();
672
715
  if (!routeItem) return void 0;
673
716
  return getParamsObject({
674
717
  params: routeItem?.params,
675
- pathname: location.pathname
718
+ pathname
676
719
  });
677
720
  };
678
721
  //#endregion
@@ -682,6 +725,12 @@ var useLoaderState = () => {
682
725
  return loaderState;
683
726
  };
684
727
  //#endregion
728
+ //#region hooks/useInvalidate.ts
729
+ var useInvalidate = () => {
730
+ const { invalidate } = useRouterActions();
731
+ return invalidate;
732
+ };
733
+ //#endregion
685
734
  //#region hooks/useBlocker.ts
686
735
  var useBlocker = (blockerFn) => {
687
736
  const { blockerState } = useNavigationState();
@@ -887,4 +936,4 @@ var adapter = {
887
936
  })
888
937
  };
889
938
  //#endregion
890
- export { Link, Router, RouterProvider, adapter, createRouter, useBeforeUnload, useBlocker, useHistoricalTrail, useLoaderState, useLocation, useNavigate, useParams, useQueryParam, useRouterContext, useSearchParams };
939
+ export { Link, Router, RouterProvider, adapter, createRouter, useBeforeUnload, useBlocker, useHistoricalTrail, useInvalidate, useLoaderState, useLocation, useNavigate, useParams, useQueryParam, useRouterContext, useSearchParams };
@@ -3,5 +3,5 @@ import { type ActionsContextValue, type DataContextValue, type NavigationContext
3
3
  type ProviderProps = NavigationContextValue & ActionsContextValue & DataContextValue & {
4
4
  children: ReactNode;
5
5
  };
6
- export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation, prefetchLoader, blockerState, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
6
+ export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation, prefetchLoader, blockerState, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState, invalidate, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
7
7
  export {};
@@ -1,18 +1,19 @@
1
- import type { ComponentType, Dispatch, ReactElement, SetStateAction } from 'react';
1
+ import type { ComponentType, Dispatch, ReactElement, ReactNode, SetStateAction } from 'react';
2
2
  export type LazyComponent = () => Promise<{
3
3
  default: ComponentType<unknown>;
4
4
  }>;
5
+ type Element = (() => ReactElement) | ReactElement;
5
6
  export type ClientRouteItem = {
6
7
  path: string;
7
- element: (() => ReactElement) | ReactElement | LazyComponent;
8
+ element: Element | LazyComponent;
8
9
  loader?(arg: {
9
10
  params: Record<string, string>;
10
11
  context: Record<string, unknown>;
11
12
  setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
12
13
  }): Promise<unknown>;
13
- loaderFallback?: (() => ReactElement) | ReactElement;
14
- errorElement?: (() => ReactElement) | ReactElement;
15
- fallback?: (() => ReactElement) | ReactElement;
14
+ loaderFallback?: Element;
15
+ errorElement?: Element;
16
+ fallback?: Element;
16
17
  children?: ClientRouteItem[];
17
18
  staleTime?: number;
18
19
  beforeLoad?: (arg: {
@@ -28,7 +29,7 @@ export type ClientRouteItem = {
28
29
  }) => Promise<void>;
29
30
  };
30
31
  export type RouteItem = ClientRouteItem & {
31
- element: (() => ReactElement) | ReactElement;
32
+ element: Element;
32
33
  params?: {
33
34
  key: string;
34
35
  value: string;
@@ -67,7 +68,11 @@ export type RouterProps = {
67
68
  animationDuration?: number;
68
69
  spinner?: boolean;
69
70
  preserveScroll?: boolean;
70
- showFallbackIfAnimated?: boolean;
71
+ showFallbackOnAnimation?: boolean;
71
72
  prefetch?: 'hover' | 'render' | 'viewport' | 'none';
72
73
  hoverPrefetchDelay?: number;
74
+ errorBoundary?: ComponentType<{
75
+ children: ReactNode;
76
+ }>;
73
77
  };
78
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {