clear-react-router 1.7.7 → 1.7.8

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
@@ -271,8 +271,8 @@ var revalidateCache = ({ routeItem, pathname }) => {
271
271
  return promise;
272
272
  };
273
273
  //#endregion
274
- //#region utils/navigationExecutor.ts
275
- var navigationExecutor = (nextLocation, routeItem) => {
274
+ //#region utils/commitState.ts
275
+ var commitState = (nextLocation, routeItem) => {
276
276
  routeItemDataState.setState({
277
277
  routeItem,
278
278
  location: nextLocation
@@ -294,6 +294,8 @@ var RouterConfig = class {
294
294
  _defineProperty(this, "isAnimated", false);
295
295
  _defineProperty(this, "showFallbackOnAnimation", false);
296
296
  _defineProperty(this, "hoverPrefetchDelay", 150);
297
+ _defineProperty(this, "beforeLoad", void 0);
298
+ _defineProperty(this, "afterLoad", void 0);
297
299
  }
298
300
  configure(config) {
299
301
  Object.assign(this, config);
@@ -301,17 +303,17 @@ var RouterConfig = class {
301
303
  };
302
304
  var routerConfig = new RouterConfig();
303
305
  //#endregion
304
- //#region utils/transitionedNavigation.ts
305
- var transitionedNavigation = (nextLocation, routeItem) => {
306
+ //#region utils/commitNavigation.ts
307
+ var commitNavigation = (nextLocation, routeItem) => {
306
308
  const { isAnimated } = routerConfig;
307
309
  if (!isAnimated || !prevPathnameRef.value) {
308
- navigationExecutor(nextLocation, routeItem);
310
+ commitState(nextLocation, routeItem);
309
311
  return;
310
312
  }
311
313
  try {
312
- document.startViewTransition(() => navigationExecutor(nextLocation, routeItem));
314
+ document.startViewTransition(() => commitState(nextLocation, routeItem));
313
315
  } catch {
314
- navigationExecutor(nextLocation, routeItem);
316
+ commitState(nextLocation, routeItem);
315
317
  }
316
318
  };
317
319
  var findRoute = (pathname, includeAll) => {
@@ -321,21 +323,24 @@ var findRoute = (pathname, includeAll) => {
321
323
  //#endregion
322
324
  //#region runtime/navigate.ts
323
325
  var navigationSeq = 0;
324
- var navigate = async (nextLocation) => {
325
- navigationSeq = navigationSeq + 1;
326
- const seq = navigationSeq;
326
+ var routeResolve = (location) => {
327
327
  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);
328
+ const nextItem = findRoute(location.pathname, true);
329
+ return {
330
+ nextItem,
331
+ params: getParamsObject({
332
+ params: nextItem?.params,
333
+ pathname: location.pathname
334
+ })
335
+ };
336
+ };
337
+ var beforeLoad = async (routeItem, params) => {
338
+ const { beforeLoad } = routerConfig;
339
+ const runBeforeLoad = async (loaderFn) => {
340
+ const redirect = async (redirected) => await navigate(typeof redirected === "string" ? { pathname: redirected } : redirected);
341
+ const { context, setContext } = getContext();
337
342
  try {
338
- await nextItem.beforeLoad({
343
+ await loaderFn({
339
344
  context,
340
345
  redirect,
341
346
  params,
@@ -350,10 +355,13 @@ var navigate = async (nextLocation) => {
350
355
  ...prev,
351
356
  beforeLoadError: error
352
357
  }));
353
- return transitionedNavigation(nextLocation, nextItem);
354
358
  }
355
- }
356
- if (seq !== navigationSeq) return;
359
+ };
360
+ if (beforeLoad) await runBeforeLoad(beforeLoad);
361
+ if (routeItem?.beforeLoad) await runBeforeLoad(routeItem?.beforeLoad);
362
+ };
363
+ var prepareNavigation = (routeItem, location) => {
364
+ const { isAnimated, showFallbackOnAnimation: showFallback } = routerConfig;
357
365
  scrollMapState.setState((prevState) => {
358
366
  const scrollPosition = document.scrollingElement?.scrollTop ?? 0;
359
367
  if (!scrollPosition || prevState[prevPathnameRef.value] === scrollPosition) return prevState;
@@ -363,24 +371,44 @@ var navigate = async (nextLocation) => {
363
371
  };
364
372
  });
365
373
  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({
374
+ routeItem,
375
+ pathname: location.pathname
376
+ }) || isAnimated && !showFallback ? void 0 : routeItem?.loaderFallback);
377
+ };
378
+ var loader = async (routeItem, location) => {
379
+ if (!routeItem?.loader) return;
380
+ isLoadingState.setState(true);
381
+ await revalidateCache({
382
+ routeItem,
383
+ pathname: location.pathname
384
+ });
385
+ };
386
+ var afterLoad = async (routeItem, params) => {
387
+ const { afterLoad } = routerConfig;
388
+ const { context, setContext } = getContext();
389
+ if (routeItem?.afterLoad) await routeItem.afterLoad({
390
+ context,
391
+ params,
392
+ setContext
393
+ });
394
+ if (afterLoad) await afterLoad({
379
395
  context,
380
396
  params,
381
397
  setContext
382
398
  });
383
399
  };
400
+ var navigate = async (nextLocation) => {
401
+ navigationSeq = navigationSeq + 1;
402
+ const seq = navigationSeq;
403
+ const { nextItem, params } = routeResolve(nextLocation);
404
+ await beforeLoad(nextItem, params);
405
+ if (seq !== navigationSeq) return;
406
+ prepareNavigation(nextItem, nextLocation);
407
+ await loader(nextItem, nextLocation);
408
+ if (seq !== navigationSeq) return;
409
+ commitNavigation(nextLocation, nextItem);
410
+ await afterLoad(nextItem, params);
411
+ };
384
412
  //#endregion
385
413
  //#region hooks/useNavigation.ts
386
414
  var useNavigation = () => {
@@ -490,7 +518,7 @@ var renderElement = (Component) => {
490
518
  //#endregion
491
519
  //#region components/Router.tsx
492
520
  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 }) => {
521
+ 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
522
  const [isLoading] = useIsLoading();
495
523
  const [currentLoaderFallback] = useLoaderFallback();
496
524
  const [routeItemData] = useRouteItemData();
@@ -501,7 +529,9 @@ var Router = ({ routes, animationDuration, isAnimated = false, spinner = true, p
501
529
  isAnimated,
502
530
  prefetch,
503
531
  hoverPrefetchDelay,
504
- showFallbackOnAnimation
532
+ showFallbackOnAnimation,
533
+ beforeLoad,
534
+ afterLoad
505
535
  });
506
536
  useApplyCustomAnimation(animationDuration);
507
537
  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.8",
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;