clear-react-router 1.8.4 → 1.8.5

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
@@ -70,7 +70,7 @@ It provides first-class support for:
70
70
 
71
71
  ### `createRouter(routes)`
72
72
 
73
- Normalizes route configuration, extracts dynamic params, builds nested paths.
73
+ Normalizes route configuration. Extracts dynamic params, builds nested paths.
74
74
 
75
75
  | Property | Type | Description |
76
76
  |----------|------|-------------|
@@ -88,14 +88,26 @@ Normalizes route configuration, extracts dynamic params, builds nested paths.
88
88
 
89
89
  ### `Link`
90
90
 
91
- Component for client-side navigation with prefetch support.
91
+ Component for client-side navigation with prefetch support, active state detection, and pending state styling.
92
92
 
93
- | Prop | Type | Default |
94
- |------|------|---------|
95
- | `to` | `string` | required |
93
+ | Prop | Type | Default | Description |
94
+ |------|------|---------|-------------|
95
+ | `to` | `string` | required | Target path |
96
96
  | `prefetch` | `'hover' \| 'render' \| 'viewport' \| 'none'` | `Router` config | Override the global prefetch strategy |
97
97
  | `hoverPrefetchDelay` | `number` | `Router` config | Override the global hover delay |
98
- | `children` | `ReactElement` | required | Content to render inside the link |
98
+ | `children` | `ReactNode` | required | Content to render inside the link |
99
+ | `className` | `string \| ({ isActive, isPending }) => string` | `undefined` | CSS class name(s). Can be a function for dynamic styling |
100
+ | `style` | `CSSProperties \| ({ isActive, isPending }) => CSSProperties` | `undefined` | Inline styles. Can be a function for dynamic styling |
101
+ | `activeClassName` | `string` optional | `'active-link'` | Class name applied when the link matches the current URL |
102
+ | `pendingClassName` | `string` optional | `'pending-link'` | Class name applied when the link's target is loading |
103
+ | `onClick` | `() => void` | `undefined` | Callback fired before navigation |
104
+
105
+ **State values:**
106
+
107
+ | State | Type | Description |
108
+ |-------|------|-------------|
109
+ | `isActive` | `boolean` | `true` when the link's `to` matches the current URL |
110
+ | `isPending` | `boolean` | `true` when the target route is currently loading (loader is running) |
99
111
 
100
112
  ### Prefetch Strategies
101
113
 
@@ -123,6 +135,21 @@ import { Router, Link } from 'clear-react-router';
123
135
  <Link to="/admin" prefetch="none">
124
136
  Admin Panel
125
137
  </Link>
138
+
139
+ // With custom active/pending classes
140
+ <Link to="/settings" activeClassName="active-nav-link" pendingClassName="loading-nav-link">
141
+ Settings
142
+ </Link>
143
+
144
+ // With dynamic className
145
+ <Link to="/dashboard" className={({ isActive, isPending }) => isActive ? 'text-blue-600' : isPending ? 'text-gray-400' : 'text-gray-600'}>
146
+ Dashboard
147
+ </Link>
148
+
149
+ // With dynamic style
150
+ <Link to="/profile" style={({ isActive }) => ({ fontWeight: isActive ? 'bold' : 'normal' })}>
151
+ Profile
152
+ </Link>
126
153
  ```
127
154
  **Important**: prefetch="render" should be used sparingly, as it preloads data immediately when the link is rendered, which may cause unnecessary network requests.
128
155
 
@@ -1,13 +1,21 @@
1
- import { type ReactElement, type MouseEvent, type CSSProperties } from 'react';
1
+ import { type CSSProperties, ReactNode } from 'react';
2
2
  import { RouterProps } from '../types';
3
3
  type LinkProps = {
4
4
  to: string;
5
- children: ReactElement<{
6
- onClick: (e: MouseEvent) => void;
7
- style: CSSProperties;
8
- }>;
5
+ children: ReactNode;
9
6
  prefetch?: RouterProps['prefetch'];
10
7
  hoverPrefetchDelay?: number;
8
+ style?: CSSProperties | (({ isActive }: {
9
+ isActive: boolean;
10
+ isPending: boolean;
11
+ }) => CSSProperties);
12
+ className?: string | (({ isActive }: {
13
+ isActive: boolean;
14
+ isPending: boolean;
15
+ }) => string);
16
+ activeClassName?: string;
17
+ pendingClassName?: string;
18
+ onClick?(): void;
11
19
  };
12
- export declare const Link: ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }: LinkProps) => import("react/jsx-runtime").JSX.Element;
20
+ export declare const Link: ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, className, style, onClick, activeClassName, pendingClassName, }: LinkProps) => import("react/jsx-runtime").JSX.Element;
13
21
  export {};
@@ -0,0 +1 @@
1
+ export declare const useIsRoutePending: (routePath: string) => boolean;
package/dist/index.js CHANGED
@@ -212,7 +212,7 @@ var findRoute = (pathname, includeAll) => {
212
212
  //#region runtime/navigate.ts
213
213
  var navigationSeq = 0;
214
214
  var createNavigate = (routerState, revalidateCache) => {
215
- const { loaderStateRef, scrollMapState, prevPathnameRef, loaderFallbackState, isLoadingState, contextState, timestampMap } = routerState;
215
+ const { loaderStateRef, scrollMapState, prevPathnameRef, loaderFallbackState, isLoadingState, contextState, timestampMap, pendingPathRef } = routerState;
216
216
  const commitNavigation = createCommitNavigation(createCommitState(routerState), prevPathnameRef);
217
217
  const isCacheItemFresh = createIsCacheItemFresh(timestampMap);
218
218
  const getContext = () => ({
@@ -269,10 +269,12 @@ var createNavigate = (routerState, revalidateCache) => {
269
269
  const loader = async (routeItem, location) => {
270
270
  if (!routeItem?.loader) return;
271
271
  isLoadingState.setState(true);
272
+ pendingPathRef.set(location.pathname);
272
273
  await revalidateCache({
273
274
  routeItem,
274
275
  pathname: location.pathname
275
276
  });
277
+ pendingPathRef.set("");
276
278
  };
277
279
  const afterLoad = async (routeItem, params) => {
278
280
  const { afterLoad } = routerConfig;
@@ -383,10 +385,10 @@ var getRetry = (routeItem) => {
383
385
  };
384
386
  var sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
385
387
  var createRevalidateCache = (routerState) => {
388
+ const { loaderStateRef, timestampMap, contextState } = routerState;
386
389
  const revalidateCache = async ({ routeItem, pathname }, retried = 0) => {
387
390
  if (!routeItem?.loader) return;
388
- const isCacheItemFresh = createIsCacheItemFresh(routerState.timestampMap);
389
- const { loaderStateRef, timestampMap, contextState } = routerState;
391
+ const isCacheItemFresh = createIsCacheItemFresh(timestampMap);
390
392
  if (loadingPromises.has(pathname)) return loadingPromises.get(pathname);
391
393
  if (isCacheItemFresh({
392
394
  routeItem,
@@ -471,6 +473,7 @@ var createRouterInstance = () => {
471
473
  }),
472
474
  loaderStateRef: new Cell(emptyLoaderState),
473
475
  prevPathnameRef: new Cell(""),
476
+ pendingPathRef: new Cell(""),
474
477
  timestampMap: /* @__PURE__ */ new Map()
475
478
  };
476
479
  const revalidateCache = createRevalidateCache(routerState);
@@ -505,7 +508,8 @@ var createRouterInstance = () => {
505
508
  scrollMapState: routerState.scrollMapState,
506
509
  contextState: routerState.contextState,
507
510
  blockedRouteState: routerState.blockedRouteState,
508
- prevPathnameRef: routerState.prevPathnameRef
511
+ prevPathnameRef: routerState.prevPathnameRef,
512
+ pendingPathRef: routerState.pendingPathRef
509
513
  },
510
514
  runtime: {
511
515
  navigate,
@@ -711,17 +715,26 @@ var Router = ({ routes, beforeLoad, afterLoad, animationDuration, isAnimated = f
711
715
  });
712
716
  };
713
717
  //#endregion
718
+ //#region hooks/useIsRoutePending.ts
719
+ var useIsRoutePending = (routePath) => {
720
+ const { hooks: { useIsLoading }, state: { pendingPathRef } } = router;
721
+ const [isPending] = useIsLoading();
722
+ return isPending && pendingPathRef.value === routePath;
723
+ };
724
+ //#endregion
714
725
  //#region hooks/useNavigate.ts
715
726
  var useNavigate = router.hooks.useNavigate;
716
727
  //#endregion
717
728
  //#region components/Link.tsx
718
- var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }) => {
719
- const { prefetch: configPrefetch, hoverPrefetchDelay: configPrefetchDelay } = routerConfig;
720
- const prefetch = prefetchLink || configPrefetch;
721
- const prefetchDelay = hoverPrefetchDelay ?? configPrefetchDelay;
729
+ var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay, className, style, onClick, activeClassName = "active-link", pendingClassName = "pending-link" }) => {
730
+ const isPending = useIsRoutePending(to);
731
+ const { pathname } = useLocation();
722
732
  const navigate = useNavigate();
723
733
  const timeout = useRef(0);
724
734
  const ref = useRef(null);
735
+ const { prefetch: configPrefetch, hoverPrefetchDelay: configPrefetchDelay } = routerConfig;
736
+ const prefetch = prefetchLink || configPrefetch;
737
+ const prefetchDelay = hoverPrefetchDelay ?? configPrefetchDelay;
725
738
  const onMouseEnter = useCallback(() => {
726
739
  if (prefetch !== "hover" || !prefetchDelay) return;
727
740
  if (timeout.current) clearTimeout(timeout.current);
@@ -756,10 +769,31 @@ var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }) => {
756
769
  if (element) observer.disconnect();
757
770
  };
758
771
  }, [prefetch, to]);
772
+ const isActive = to === pathname;
773
+ const normalizedClassName = typeof className === "function" ? className({
774
+ isActive,
775
+ isPending
776
+ }) : className;
777
+ const normalizedStyle = typeof style === "function" ? style({
778
+ isActive,
779
+ isPending
780
+ }) : style;
781
+ const resultClassName = [
782
+ isActive && activeClassName,
783
+ isPending && pendingClassName,
784
+ normalizedClassName
785
+ ].filter(Boolean).join(" ");
786
+ const clickHandler = async (event) => {
787
+ event.preventDefault();
788
+ onClick?.();
789
+ await navigate(to);
790
+ };
759
791
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
792
+ href: to,
760
793
  ref,
761
- style: { cursor: "pointer" },
762
- onClick: () => navigate(to),
794
+ style: normalizedStyle,
795
+ className: resultClassName,
796
+ onClick: clickHandler,
763
797
  onMouseEnter,
764
798
  onMouseLeave,
765
799
  children
package/dist/types.d.ts CHANGED
@@ -103,6 +103,7 @@ export type RouterState = {
103
103
  }>;
104
104
  loaderStateRef: Cell<LoaderState>;
105
105
  prevPathnameRef: Cell<string>;
106
+ pendingPathRef: Cell<string>;
106
107
  timestampMap: Map<string, number>;
107
108
  };
108
109
  export type RouterType = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.8.4",
3
+ "version": "1.8.5",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {