clear-react-router 1.7.7 → 1.7.9

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
@@ -64,6 +64,8 @@ Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic pa
64
64
  | `animationDuration` | `number` | `optional` | Animation duration in milliseconds (browser default is used if not set) |
65
65
  | `defaultLoaderFallback` | `ReactElement \| () => ReactElement` | `optional` | Default loading fallback for every route loader |
66
66
  | `defaultErrorElement` | `ReactElement \| () => ReactElement` | `optional` | Default error fallback for every route |
67
+ | `beforeLoad` | `({ params, context, redirect, setContext }) => Promise<unknown> \| undefined \| void` | `undefined` | Runs before every navigation. Useful for authentication, analytics, or updating shared context. |
68
+ | `afterLoad` | `({ params, context, setContext }) => Promise<void>` | `undefined` | Runs after every successful navigation. Useful for analytics, page tracking, or other global side effects. |
67
69
  | `spinner` | `boolean \| undefined` | `true` | Show a small spinner in the corner while loading data (only when `isAnimated` is enabled) |
68
70
  | `preserveScroll` | `boolean \| undefined` | `true` | Save and restore scroll position when navigating between pages |
69
71
  | `showFallbackOnAnimation` | `boolean \| undefined` | `false` | Show `loaderFallback` even when `isAnimated` is `true` (instead of spinner) |
@@ -72,6 +74,8 @@ Normalizes route configuration. Handles wildcard `*` routes, extracts dynamic pa
72
74
  | `context` | `object` | `{}` | Initial context (user, theme, etc.) |
73
75
  | `errorBoundary` | `ComponentType<{ children: ReactNode }>` | `undefined` | Custom error boundary component for catching render errors in route components |
74
76
 
77
+ > **Note:** Global lifecycle hooks wrap every route navigation. The global beforeLoad runs **before** the route-specific beforeLoad, while the global afterLoad runs **after** the route-specific afterLoad.
78
+
75
79
  ```tsx
76
80
  <div>
77
81
  <Navbar />
@@ -1,2 +1,2 @@
1
1
  import { RouterProps } from '../types/global';
2
- export declare const Router: ({ routes, animationDuration, isAnimated, spinner, preserveScroll, showFallbackOnAnimation, prefetch, hoverPrefetchDelay, errorBoundary: ErrorBoundary, context: initialContext, defaultLoaderFallback, defaultErrorElement, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
2
+ export declare const Router: ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated, spinner, preserveScroll, showFallbackOnAnimation, prefetch, hoverPrefetchDelay, errorBoundary: ErrorBoundary, context: initialContext, defaultLoaderFallback, defaultErrorElement, }: RouterProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -1,10 +1,12 @@
1
- import { RouterProps } from '../types/global';
1
+ import { ClientRouteItem, RouterProps } from '../types/global';
2
2
  declare class RouterConfig {
3
3
  routes: RouterProps['routes'];
4
4
  prefetch: RouterProps['prefetch'];
5
5
  isAnimated: RouterProps['isAnimated'];
6
6
  showFallbackOnAnimation: RouterProps['showFallbackOnAnimation'];
7
7
  hoverPrefetchDelay: number;
8
+ beforeLoad?: ClientRouteItem['beforeLoad'];
9
+ afterLoad?: ClientRouteItem['afterLoad'];
8
10
  configure(config: Partial<RouterConfig>): void;
9
11
  }
10
12
  export declare const routerConfig: RouterConfig;
package/dist/index.js CHANGED
@@ -210,6 +210,7 @@ var parseWindowLocation = (location) => ({
210
210
  search: location.search
211
211
  });
212
212
  var comparePaths = (el, pathname) => {
213
+ if (!el.params?.length) return el.path.replaceAll("/", "") === pathname.replaceAll("/", "");
213
214
  const splitElementPath = el.path.split("/").filter(Boolean);
214
215
  const paramsLength = el.params ? Object.keys(el.params).length : 0;
215
216
  const splitPathname = pathname.split("/").filter(Boolean);
@@ -271,8 +272,8 @@ var revalidateCache = ({ routeItem, pathname }) => {
271
272
  return promise;
272
273
  };
273
274
  //#endregion
274
- //#region utils/navigationExecutor.ts
275
- var navigationExecutor = (nextLocation, routeItem) => {
275
+ //#region utils/commitState.ts
276
+ var commitState = (nextLocation, routeItem) => {
276
277
  routeItemDataState.setState({
277
278
  routeItem,
278
279
  location: nextLocation
@@ -294,6 +295,8 @@ var RouterConfig = class {
294
295
  _defineProperty(this, "isAnimated", false);
295
296
  _defineProperty(this, "showFallbackOnAnimation", false);
296
297
  _defineProperty(this, "hoverPrefetchDelay", 150);
298
+ _defineProperty(this, "beforeLoad", void 0);
299
+ _defineProperty(this, "afterLoad", void 0);
297
300
  }
298
301
  configure(config) {
299
302
  Object.assign(this, config);
@@ -301,17 +304,17 @@ var RouterConfig = class {
301
304
  };
302
305
  var routerConfig = new RouterConfig();
303
306
  //#endregion
304
- //#region utils/transitionedNavigation.ts
305
- var transitionedNavigation = (nextLocation, routeItem) => {
307
+ //#region utils/commitNavigation.ts
308
+ var commitNavigation = (nextLocation, routeItem) => {
306
309
  const { isAnimated } = routerConfig;
307
310
  if (!isAnimated || !prevPathnameRef.value) {
308
- navigationExecutor(nextLocation, routeItem);
311
+ commitState(nextLocation, routeItem);
309
312
  return;
310
313
  }
311
314
  try {
312
- document.startViewTransition(() => navigationExecutor(nextLocation, routeItem));
315
+ document.startViewTransition(() => commitState(nextLocation, routeItem));
313
316
  } catch {
314
- navigationExecutor(nextLocation, routeItem);
317
+ commitState(nextLocation, routeItem);
315
318
  }
316
319
  };
317
320
  var findRoute = (pathname, includeAll) => {
@@ -321,25 +324,26 @@ var findRoute = (pathname, includeAll) => {
321
324
  //#endregion
322
325
  //#region runtime/navigate.ts
323
326
  var navigationSeq = 0;
324
- var navigate = async (nextLocation) => {
325
- navigationSeq = navigationSeq + 1;
326
- const seq = navigationSeq;
327
+ var routeResolve = (location) => {
327
328
  loaderStateRef.set(emptyLoaderState);
328
- const { isAnimated, showFallbackOnAnimation: showFallback } = routerConfig;
329
- const nextItem = findRoute(nextLocation.pathname, true);
330
- const params = getParamsObject({
331
- params: nextItem?.params,
332
- pathname: nextLocation.pathname
333
- });
334
- const { context, setContext } = getContext();
335
- if (nextItem?.beforeLoad) {
336
- const redirect = async (location) => await navigate(typeof location === "string" ? { pathname: location } : location);
329
+ const nextItem = findRoute(location.pathname, true);
330
+ return {
331
+ nextItem,
332
+ params: getParamsObject({
333
+ params: nextItem?.params,
334
+ pathname: location.pathname
335
+ })
336
+ };
337
+ };
338
+ var beforeLoad = async (routeItem, params) => {
339
+ const { beforeLoad } = routerConfig;
340
+ const runBeforeLoad = async (loaderFn) => {
341
+ const redirect = async (redirected) => await navigate(typeof redirected === "string" ? { pathname: redirected } : redirected);
337
342
  try {
338
- await nextItem.beforeLoad({
339
- context,
343
+ await loaderFn({
344
+ ...getContext(),
340
345
  redirect,
341
- params,
342
- setContext
346
+ params
343
347
  });
344
348
  loaderStateRef.set((prev) => ({
345
349
  ...prev,
@@ -350,10 +354,13 @@ var navigate = async (nextLocation) => {
350
354
  ...prev,
351
355
  beforeLoadError: error
352
356
  }));
353
- return transitionedNavigation(nextLocation, nextItem);
354
357
  }
355
- }
356
- if (seq !== navigationSeq) return;
358
+ };
359
+ if (beforeLoad) await runBeforeLoad(beforeLoad);
360
+ if (routeItem?.beforeLoad) await runBeforeLoad(routeItem?.beforeLoad);
361
+ };
362
+ var prepareNavigation = (routeItem, location) => {
363
+ const { isAnimated, showFallbackOnAnimation: showFallback } = routerConfig;
357
364
  scrollMapState.setState((prevState) => {
358
365
  const scrollPosition = document.scrollingElement?.scrollTop ?? 0;
359
366
  if (!scrollPosition || prevState[prevPathnameRef.value] === scrollPosition) return prevState;
@@ -363,24 +370,41 @@ var navigate = async (nextLocation) => {
363
370
  };
364
371
  });
365
372
  loaderFallbackState.setState(isCacheItemFresh({
366
- routeItem: nextItem,
367
- pathname: nextLocation.pathname
368
- }) || isAnimated && !showFallback ? void 0 : nextItem?.loaderFallback);
369
- if (nextItem?.loader) {
370
- isLoadingState.setState(true);
371
- await revalidateCache({
372
- routeItem: nextItem,
373
- pathname: nextLocation.pathname
374
- });
375
- }
376
- if (seq !== navigationSeq) return;
377
- transitionedNavigation(nextLocation, nextItem);
378
- if (nextItem?.afterLoad) await nextItem.afterLoad({
379
- context,
380
- params,
381
- setContext
373
+ routeItem,
374
+ pathname: location.pathname
375
+ }) || isAnimated && !showFallback ? void 0 : routeItem?.loaderFallback);
376
+ };
377
+ var loader = async (routeItem, location) => {
378
+ if (!routeItem?.loader) return;
379
+ isLoadingState.setState(true);
380
+ await revalidateCache({
381
+ routeItem,
382
+ pathname: location.pathname
382
383
  });
383
384
  };
385
+ var afterLoad = async (routeItem, params) => {
386
+ const { afterLoad } = routerConfig;
387
+ if (routeItem?.afterLoad) await routeItem.afterLoad({
388
+ ...getContext(),
389
+ params
390
+ });
391
+ if (afterLoad) await afterLoad({
392
+ ...getContext(),
393
+ params
394
+ });
395
+ };
396
+ var navigate = async (nextLocation) => {
397
+ navigationSeq = navigationSeq + 1;
398
+ const seq = navigationSeq;
399
+ const { nextItem, params } = routeResolve(nextLocation);
400
+ await beforeLoad(nextItem, params);
401
+ if (seq !== navigationSeq) return;
402
+ prepareNavigation(nextItem, nextLocation);
403
+ await loader(nextItem, nextLocation);
404
+ if (seq !== navigationSeq) return;
405
+ commitNavigation(nextLocation, nextItem);
406
+ await afterLoad(nextItem, params);
407
+ };
384
408
  //#endregion
385
409
  //#region hooks/useNavigation.ts
386
410
  var useNavigation = () => {
@@ -490,7 +514,7 @@ var renderElement = (Component) => {
490
514
  //#endregion
491
515
  //#region components/Router.tsx
492
516
  var EmptyBoundary = ({ children }) => children;
493
- var Router = ({ routes, animationDuration, isAnimated = false, spinner = true, preserveScroll = true, showFallbackOnAnimation = false, prefetch = "hover", hoverPrefetchDelay = 150, errorBoundary: ErrorBoundary = EmptyBoundary, context: initialContext, defaultLoaderFallback, defaultErrorElement }) => {
517
+ var Router = ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated = false, spinner = true, preserveScroll = true, showFallbackOnAnimation = false, prefetch = "hover", hoverPrefetchDelay = 150, errorBoundary: ErrorBoundary = EmptyBoundary, context: initialContext, defaultLoaderFallback, defaultErrorElement }) => {
494
518
  const [isLoading] = useIsLoading();
495
519
  const [currentLoaderFallback] = useLoaderFallback();
496
520
  const [routeItemData] = useRouteItemData();
@@ -501,7 +525,9 @@ var Router = ({ routes, animationDuration, isAnimated = false, spinner = true, p
501
525
  isAnimated,
502
526
  prefetch,
503
527
  hoverPrefetchDelay,
504
- showFallbackOnAnimation
528
+ showFallbackOnAnimation,
529
+ beforeLoad,
530
+ afterLoad
505
531
  });
506
532
  useApplyCustomAnimation(animationDuration);
507
533
  useSetInitialContext(initialContext);
@@ -3,6 +3,17 @@ export type LazyComponent = () => Promise<{
3
3
  default: ComponentType<unknown>;
4
4
  }>;
5
5
  export type RenderElement = (() => ReactElement) | ReactElement;
6
+ export type BeforeLoad = (arg: {
7
+ context: Record<string, unknown>;
8
+ redirect: (arg: Location | string) => Promise<void>;
9
+ params: Record<string, string>;
10
+ setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
11
+ }) => Promise<unknown> | undefined | void;
12
+ export type AfterLoad = (arg: {
13
+ context: Record<string, unknown>;
14
+ params: Record<string, string>;
15
+ setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
16
+ }) => Promise<void>;
6
17
  export type ClientRouteItem = {
7
18
  path: string;
8
19
  element: RenderElement | LazyComponent;
@@ -16,12 +27,7 @@ export type ClientRouteItem = {
16
27
  fallback?: RenderElement;
17
28
  children?: ClientRouteItem[];
18
29
  staleTime?: number;
19
- beforeLoad?: (arg: {
20
- context: Record<string, unknown>;
21
- redirect: (arg: Location | string) => Promise<void>;
22
- params: Record<string, string>;
23
- setContext: Dispatch<SetStateAction<Record<string, unknown>>>;
24
- }) => Promise<unknown> | undefined | void;
30
+ beforeLoad?: BeforeLoad;
25
31
  afterLoad?: (arg: {
26
32
  context: Record<string, unknown>;
27
33
  params: Record<string, string>;
@@ -79,5 +85,7 @@ export type RouterProps = {
79
85
  errorBoundary?: ComponentType<{
80
86
  children: ReactNode;
81
87
  }>;
88
+ beforeLoad?: ClientRouteItem['beforeLoad'];
89
+ afterLoad?: ClientRouteItem['afterLoad'];
82
90
  context?: Record<string, unknown>;
83
91
  };
@@ -0,0 +1,2 @@
1
+ import { Location, RouteItem } from '../types/global';
2
+ export declare const commitNavigation: (nextLocation: Location, routeItem: RouteItem | undefined) => void;
@@ -0,0 +1,2 @@
1
+ import { Location, RouteItem } from '../types/global';
2
+ export declare const commitState: (nextLocation: Location, routeItem: RouteItem | undefined) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.7.7",
3
+ "version": "1.7.9",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {
@@ -1,2 +0,0 @@
1
- import { Location, RouteItem } from '../types/global';
2
- export declare const navigationExecutor: (nextLocation: Location, routeItem: RouteItem | undefined) => void;
@@ -1,2 +0,0 @@
1
- import { Location, RouteItem } from '../types/global';
2
- export declare const transitionedNavigation: (nextLocation: Location, routeItem: RouteItem | undefined) => void;