clear-react-router 1.6.3 → 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
@@ -20,7 +20,7 @@ It provides first-class support for:
20
20
  - **Navigation Blocking** - Prevent accidental navigation with `useBlocker`
21
21
  - **Smooth Animations** - Page transitions with fade effect (customizable duration)
22
22
  - **Static Layout** — Keep navbar, footer, and other elements outside the router to avoid unnecessary re-renders
23
- - **Type-safe Redirects** - Redirect from beforeLoad hook
23
+ - **Programmatic Redirects** - Redirect from beforeLoad hook
24
24
  - **Cache invalidation** - Manual route invalidation
25
25
  - **Prefetching** - Preload data on hover for instant navigation
26
26
  - **Lazy Loading** - Code-split your routes with dynamic imports for optimal performance
@@ -55,7 +55,7 @@ The root component that provides routing context to the application. Place stati
55
55
 
56
56
  | Prop | Type | Default | Description |
57
57
  |------|------|---------|-------------|
58
- | `routeList` | `RouteItem[]` | required | Array of route configurations |
58
+ | `routes` | `RouteItem[]` | required | Array of route configurations |
59
59
  | `context` | `object` | `{}` | Initial context (user, theme, etc.) |
60
60
  | `children` | `ReactNode` | required | App content (must include `<Router />`) |
61
61
 
@@ -83,12 +83,13 @@ Renders the current route's component. Must be placed inside `<RouterProvider>`.
83
83
  | `animationDuration` | `number` | `optional` | Animation duration in milliseconds (browser default is used if not set) |
84
84
  | `spinner` | `boolean \| undefined` | `true` | Show a small spinner in the corner while loading data (only when `isAnimated` is enabled) |
85
85
  | `preserveScroll` | `boolean \| undefined` | `true` | Save and restore scroll position when navigating between pages |
86
- | `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) |
87
87
  | `prefetch` | `'hover' \| 'render' \| 'viewport' \| 'none'` | `'hover'` | Default prefetch strategy for all `<Link>` components |
88
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 |
89
90
 
90
91
  ```
91
- <RouterProvider routeList={routes}>
92
+ <RouterProvider routes={routes}>
92
93
  <Navbar />
93
94
  <Router spinner={false} isAnimated /> {/* disable the spinner */}
94
95
  </RouterProvider>
@@ -122,7 +123,7 @@ Component for client-side navigation with prefetch support.
122
123
  import { RouterProvider, Router, Link } from 'clear-react-router';
123
124
 
124
125
  // Global prefetch: hover with 100ms delay
125
- <RouterProvider routeList={routes} prefetch="hover" hoverPrefetchDelay={100}>
126
+ <RouterProvider routes={routes} prefetch="hover" hoverPrefetchDelay={100}>
126
127
  <Router />
127
128
  </RouterProvider>
128
129
 
@@ -216,6 +217,23 @@ const routes = createRouter([
216
217
  ]);
217
218
  ```
218
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
+
219
237
  ## Hooks
220
238
 
221
239
  ### `useNavigate()`
@@ -285,7 +303,7 @@ const UserProfile = () => {
285
303
  }
286
304
  ```
287
305
 
288
- ### `useInvalidate`
306
+ ### `useInvalidate()`
289
307
 
290
308
  Returns a function that marks route data as stale and re-runs the route lifecycle.
291
309
 
@@ -566,7 +584,6 @@ function ProductFilter() {
566
584
 
567
585
  - **Array support** — `getSearchParams` returns `string[]` when multiple values exist for the same key
568
586
  - **Functional updates** — Update parameters based on previous state without losing other params
569
- - **Type-safe** — Proper TypeScript support with overloads
570
587
  - **Stable reference** — `setSearchParams` reference is stable and safe to use in `useEffect`
571
588
 
572
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;
@@ -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>>>;
@@ -12,13 +12,13 @@ type UseHandleNavigation = {
12
12
  loaderStateRef: RefObject<LoaderState>;
13
13
  clearTimestamp(path: string): void;
14
14
  };
15
- export declare const useHandleNavigation: ({ routeList, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef, clearTimestamp, }: UseHandleNavigation) => {
15
+ export declare const useHandleNavigation: ({ routes, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef, clearTimestamp, }: UseHandleNavigation) => {
16
16
  blockerState: BlockerState;
17
17
  updateLocation: (nextLocation: Location) => Promise<void>;
18
18
  updateBlockedRoute: ({ type, payload }: UpdateBlockedRouteProps) => void;
19
19
  routeItemData: RouteItemData;
20
20
  restoreScroll: () => void;
21
- 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;
22
22
  isLoading: boolean;
23
23
  loaderState: LoaderState;
24
24
  invalidate: (pathname?: string) => Promise<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 }: {
package/dist/index.d.ts CHANGED
@@ -14,4 +14,4 @@ export { useSearchParams } from './hooks/useSearchParams';
14
14
  export { useHistoricalTrail } from './hooks/useHistoricalTrail';
15
15
  export { adapter } from './utils/adapter';
16
16
  export { createRouter } from './utils/utils';
17
- 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
@@ -125,7 +125,7 @@ function _defineProperty(e, r, t) {
125
125
  var RouterConfig = class {
126
126
  constructor() {
127
127
  _defineProperty(this, "isAnimated", false);
128
- _defineProperty(this, "showFallbackIfAnimated", false);
128
+ _defineProperty(this, "showFallbackOnAnimation", false);
129
129
  _defineProperty(this, "prefetch", "hover");
130
130
  _defineProperty(this, "hoverPrefetchDelay", 150);
131
131
  }
@@ -188,8 +188,8 @@ var comparePaths = (el, pathname) => {
188
188
  //#endregion
189
189
  //#region hooks/useHandleNavigation.ts
190
190
  var ALL_LOCATIONS = "*";
191
- var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef, clearTimestamp }) => {
192
- const { isAnimated, showFallbackIfAnimated: showFallback } = routerConfig;
191
+ var useHandleNavigation = ({ routes, context, revalidateCache, setContext, isCacheItemFresh, loaderStateRef, clearTimestamp }) => {
192
+ const { isAnimated, showFallbackOnAnimation: showFallback } = routerConfig;
193
193
  const [isLoading, setIsLoading] = useState(false);
194
194
  const [blockedRoute, setBlockedRoute] = useState({
195
195
  from: "",
@@ -240,7 +240,7 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
240
240
  }, [navigation, isAnimated]);
241
241
  const invalidate = useCallback(async (pathname = routeItemData.location.pathname) => {
242
242
  if (typeof pathname !== "string") return;
243
- const routeItem = routeList.find((el) => comparePaths(el, pathname));
243
+ const routeItem = routes.find((el) => comparePaths(el, pathname));
244
244
  const resultParams = getParamsObject({
245
245
  params: routeItem?.params,
246
246
  pathname
@@ -274,14 +274,14 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
274
274
  loaderStateRef,
275
275
  revalidateCache,
276
276
  routeItemData.location.pathname,
277
- routeList,
277
+ routes,
278
278
  setContext
279
279
  ]);
280
280
  const navigationHandler = useCallback(async (nextLocation) => {
281
281
  navigationSeq.current = navigationSeq.current + 1;
282
282
  const seq = navigationSeq.current;
283
283
  loaderStateRef.current = emptyLoaderState;
284
- const nextItem = routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, nextLocation.pathname));
284
+ const nextItem = routes.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, nextLocation.pathname));
285
285
  const params = getParamsObject({
286
286
  params: nextItem?.params,
287
287
  pathname: nextLocation.pathname
@@ -337,7 +337,7 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
337
337
  }, [
338
338
  context,
339
339
  revalidateCache,
340
- routeList,
340
+ routes,
341
341
  transitionedNavigation,
342
342
  setContext,
343
343
  isCacheItemFresh,
@@ -407,7 +407,7 @@ var useHandleNavigation = ({ routeList, context, revalidateCache, setContext, is
407
407
  };
408
408
  //#endregion
409
409
  //#region hooks/useLoader.ts
410
- var useLoader = ({ routeList, context, setContext }) => {
410
+ var useLoader = ({ routes, context, setContext }) => {
411
411
  const timestampMapRef = useRef(/* @__PURE__ */ new Map());
412
412
  const loaderMapRef = useRef({});
413
413
  const loaderStateRef = useRef(emptyLoaderState);
@@ -467,12 +467,12 @@ var useLoader = ({ routeList, context, setContext }) => {
467
467
  ]);
468
468
  return {
469
469
  prefetchLoader: useCallback(async (pathname) => {
470
- const item = routeList.find((el) => comparePaths(el, pathname));
470
+ const item = routes.find((el) => comparePaths(el, pathname));
471
471
  if (item) await revalidateCache({
472
472
  routeItem: item,
473
473
  pathname
474
474
  });
475
- }, [revalidateCache, routeList]),
475
+ }, [revalidateCache, routes]),
476
476
  revalidateCache,
477
477
  isCacheItemFresh,
478
478
  loaderStateRef,
@@ -483,15 +483,15 @@ var useLoader = ({ routeList, context, setContext }) => {
483
483
  };
484
484
  //#endregion
485
485
  //#region components/RouterProvider.tsx
486
- var RouterProvider = ({ children, routeList, context: initialContext = {} }) => {
486
+ var RouterProvider = ({ children, routes, context: initialContext = {} }) => {
487
487
  const [context, setContext] = useState(initialContext);
488
488
  const { prefetchLoader, revalidateCache, isCacheItemFresh, loaderStateRef, clearTimestamp } = useLoader({
489
- routeList,
489
+ routes,
490
490
  context,
491
491
  setContext
492
492
  });
493
493
  const { blockerState, updateLocation, updateBlockedRoute, routeItemData, restoreScroll, currentLoaderFallback, isLoading, loaderState, invalidate } = useHandleNavigation({
494
- routeList,
494
+ routes,
495
495
  context,
496
496
  setContext,
497
497
  revalidateCache,
@@ -590,6 +590,11 @@ var usePreserveScroll = (preserveScroll) => {
590
590
  ]);
591
591
  };
592
592
  //#endregion
593
+ //#region hooks/useSetRouterConfig.ts
594
+ var useSetRouterConfig = (routerProps) => {
595
+ useEffect(() => routerConfig.configure(routerProps), [routerProps]);
596
+ };
597
+ //#endregion
593
598
  //#region components/Spinner.tsx
594
599
  var Spinner = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cr-spinner" });
595
600
  //#endregion
@@ -599,32 +604,28 @@ var renderElement = (Component) => {
599
604
  return typeof Component === "function" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, {}) : Component;
600
605
  };
601
606
  //#endregion
602
- //#region hooks/useSetRouterConfig.ts
603
- var useSetRouterConfig = (routerProps) => {
604
- useEffect(() => routerConfig.configure(routerProps), [routerProps]);
605
- };
606
- //#endregion
607
607
  //#region components/Router.tsx
608
- var Router = ({ isAnimated, animationDuration, spinner = true, preserveScroll = true, showFallbackIfAnimated = false, prefetch = "hover", hoverPrefetchDelay = 150 }) => {
609
- 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();
610
611
  usePreserveScroll(preserveScroll);
611
612
  useApplyCustomAnimation(animationDuration);
612
613
  useSetRouterConfig({
613
614
  isAnimated,
614
- showFallbackIfAnimated,
615
+ showFallbackOnAnimation,
615
616
  prefetch,
616
617
  hoverPrefetchDelay
617
618
  });
618
619
  const showErrorElement = !isLoading && Boolean(loaderState.loaderError || loaderState.beforeLoadError);
619
620
  const showSpinner = spinner && isAnimated && isLoading;
620
621
  const loadingContent = !showErrorElement && isLoading;
621
- if ((showFallbackIfAnimated || !isAnimated) && loadingContent) return renderElement(currentLoaderFallback);
622
- 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, {});
623
624
  if (!routeItem) return null;
624
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, {})] });
625
626
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
626
627
  style: { viewTransitionName: "page" },
627
- 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, {})]
628
629
  });
629
630
  };
630
631
  //#endregion
@@ -710,11 +711,11 @@ var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }) => {
710
711
  //#endregion
711
712
  //#region hooks/useParams.ts
712
713
  var useParams = () => {
713
- const { routeItemData: { routeItem } } = useNavigationState();
714
+ const { routeItemData: { routeItem, location: { pathname } } } = useNavigationState();
714
715
  if (!routeItem) return void 0;
715
716
  return getParamsObject({
716
717
  params: routeItem?.params,
717
- pathname: location.pathname
718
+ pathname
718
719
  });
719
720
  };
720
721
  //#endregion
@@ -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.3",
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": {