clear-react-router 1.9.4 → 1.9.6

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
@@ -107,7 +107,7 @@ Component for client-side navigation with prefetch support, active state detecti
107
107
  | Prop | Type | Default | Description |
108
108
  |------|------|---------|-------------|
109
109
  | `to` | `string` | required | Target path |
110
- | `as` | `(props: ElementProps<T>) => ReactElement` | renders `<a>` | Render prop for using a custom element/component instead of the default `<a>`. Receives the computed isActive and isPending values, event handlers, and ref to attach to your own element |
110
+ | `as` | `(props: ElementProps<T>, state: { isActive: boolean; isPending: boolean }) => ReactElement` | renders `<a>` | Render function for using a custom element/component instead of the default <a>. Receives the props to spread onto your element (href, ref, event handlers, className, style, children) as the first argument, and `{ isActive, isPending }` as a separate second argument — kept separate so these values are never accidentally forwarded to the DOM |
111
111
  | `exact` | `boolean` | `false` | When `false`, the link is also considered active if the current URL starts with `to` (useful for nested routes) |
112
112
  | `prefetch` | `'hover' \| 'render' \| 'viewport' \| 'none'` | `Router` config | Override the global prefetch strategy |
113
113
  | `hoverPrefetchDelay` | `number` | `Router` config | Override the global hover delay |
@@ -134,27 +134,68 @@ Component for client-side navigation with prefetch support, active state detecti
134
134
  | `'viewport'` | Prefetches when the link enters the viewport (using Intersection Observer) |
135
135
  | `'none'` | No prefetching |
136
136
 
137
+ ### Custom elements via `as`
138
+
139
+ When using `as` to render a custom component instead of the default `<a>`, your component **must spread all received props onto the underlying host element** — including `ref`. If any prop is dropped, the corresponding feature silently stops working (no error is thrown):
140
+
141
+ - Missing `ref` → `viewport` prefetch never triggers (the `IntersectionObserver` has nothing to observe).
142
+ - Missing `onClick` → navigation doesn't happen, the link just does nothing.
143
+ - Missing `onMouseEnter`/`onMouseLeave` → `hover` prefetch doesn't trigger.
144
+ - Missing `href` → the link isn't reachable via keyboard, screen readers, "open in new tab", etc.
145
+
146
+ ```tsx
147
+ // ✅ correct — every prop is forwarded to the host element
148
+ const Button = ({ children, ...props }: ElementProps<HTMLButtonElement>) => (
149
+ <button {...props}>{children}</button>
150
+ );
151
+
152
+ // ❌ wrong — ref, event handlers, and href are silently dropped
153
+ const Button = ({ children }: { children: ReactNode }) => (
154
+ <button>{children}</button>
155
+ );
156
+ ```
157
+
158
+ If you only want to add or override specific props (e.g. add a `variant`), spread the received props first, then apply your own on top:
159
+
160
+ ```tsx
161
+ const Button = ({ children, ...props }: ElementProps<HTMLButtonElement>) => (
162
+ <button {...props} className={`btn ${props.className ?? ''}`}>
163
+ {children}
164
+ </button>
165
+ );
166
+ ```
167
+
168
+ > **Note:** Because `as` is called as a plain function rather than rendered via JSX, avoid using React hooks (`useState`, `useEffect`, etc.) inside the function you pass to `as` — it isn't tracked by React as a separate component in the fiber tree. A function written for `as` (like `Button` above, which takes a second `state` argument) also isn't a valid standalone React component and shouldn't be rendered directly as `<Button />` elsewhere.
169
+
137
170
  **Example:**
138
171
 
139
172
  ```tsx
140
- import { Router, Link } from 'clear-react-router';
173
+ import { Link, type ElementProps } from 'clear-react-router';
174
+
175
+ const Button = (
176
+ { children, ...rest }: ElementProps<HTMLButtonElement>,
177
+ { isActive }: { isActive: boolean }
178
+ ) => (
179
+ <button {...rest} style={{ background: isActive ? 'tomato' : 'green' }}>
180
+ {children}
181
+ </button>
182
+ );
183
+
184
+ <Link to="/about" as={Button}>To about page</Link>
185
+
186
+ For third-party components (MUI, Chakra, etc.), wrap them in an inline arrow function — most of them accept a
187
+ single `props` argument and forward it to the host element themselves:
141
188
 
142
- // Render a custom element/component via `as`. The function receives ref, event handlers, isActive/isPending and must render them itself
143
189
  import { Button } from '@mui/material';
144
190
 
145
191
  <Link
146
- to="/dashboard"
147
- as={({ isActive, isPending, ...props }) => (
148
- <Button
149
- {...props}
150
- variant={isActive ? 'contained' : 'outlined'}
151
- sx={{ opacity: isPending ? 0.5 : 1 }}
152
- />
153
- )}
192
+ to="/about"
193
+ as={(props, { isActive }) => <Button {...props} variant={isActive ? 'contained' : 'text'} />}
154
194
  >
155
- Dashboard
195
+ To about page
156
196
  </Link>
157
-
197
+ ```
198
+ ```tsx
158
199
  // Global prefetch: hover with 100ms delay
159
200
  <Router routes={routes} prefetch="hover" hoverPrefetchDelay={100} />
160
201
 
@@ -1,32 +1,20 @@
1
- import { type CSSProperties, ReactNode, MouseEvent, ReactElement, Ref } from 'react';
2
- import { RouterProps } from '../types';
3
- type States = {
1
+ import { type CSSProperties, ReactNode, ReactElement } from 'react';
2
+ import { ElementProps, RouterProps } from '../types';
3
+ type ElementState = {
4
4
  isActive: boolean;
5
5
  isPending: boolean;
6
6
  };
7
- type ElementProps<T extends HTMLElement = HTMLElement> = {
8
- ref: Ref<T>;
9
- href: string;
10
- isActive: boolean;
11
- isPending: boolean;
12
- onClick(event: MouseEvent): void;
13
- onMouseEnter(event: MouseEvent): void;
14
- onMouseLeave(event: MouseEvent): void;
15
- className?: string;
16
- style?: CSSProperties;
17
- children?: ReactNode;
18
- };
19
7
  type LinkProps<T extends HTMLElement = HTMLAnchorElement> = {
20
8
  to: string;
21
9
  children?: ReactNode;
22
- as?: (props: ElementProps<T>) => ReactElement;
10
+ as?: (props: ElementProps<T>, state: ElementState) => ReactElement;
23
11
  prefetch?: RouterProps['prefetch'];
24
12
  hoverPrefetchDelay?: number;
25
- className?: string | ((arg: States) => string);
13
+ className?: string | ((arg: ElementState) => string);
26
14
  activeClassName?: string;
27
15
  pendingClassName?: string;
28
16
  beforeNavigate?(): Promise<void>;
29
- style?: CSSProperties | ((arg: States) => CSSProperties);
17
+ style?: CSSProperties | ((arg: ElementState) => CSSProperties);
30
18
  exact?: boolean;
31
19
  };
32
20
  export declare const Link: <T extends HTMLElement = HTMLAnchorElement>({ children, to, as, prefetch: prefetchLink, hoverPrefetchDelay, className, style, beforeNavigate, exact, activeClassName, pendingClassName, }: LinkProps<T>) => ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
package/dist/index.d.ts CHANGED
@@ -12,4 +12,4 @@ export { useRouterContext } from './hooks/useRouterContext';
12
12
  export { useSearchParams } from './hooks/useSearchParams';
13
13
  export { useFormContext } from './hooks/useFormContext';
14
14
  export { createRouter } from './utils/utils';
15
- export type { RouteItem, BlockerState, Location, RouterProps } from './types';
15
+ export type { RouteItem, BlockerState, Location, RouterProps, ElementProps } from './types';
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ var createCommitState = ({ routeItemDataState, pendingState }) => (nextLocation,
44
44
  //#region constants.ts
45
45
  var emptyLoaderState = {};
46
46
  //#endregion
47
- //#region \0@oxc-project+runtime@0.132.0/helpers/typeof.js
47
+ //#region \0@oxc-project+runtime@0.143.0/helpers/esm/typeof.js
48
48
  function _typeof(o) {
49
49
  "@babel/helpers - typeof";
50
50
  return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
@@ -54,7 +54,7 @@ function _typeof(o) {
54
54
  }, _typeof(o);
55
55
  }
56
56
  //#endregion
57
- //#region \0@oxc-project+runtime@0.132.0/helpers/toPrimitive.js
57
+ //#region \0@oxc-project+runtime@0.143.0/helpers/esm/toPrimitive.js
58
58
  function toPrimitive(t, r) {
59
59
  if ("object" != _typeof(t) || !t) return t;
60
60
  var e = t[Symbol.toPrimitive];
@@ -66,13 +66,13 @@ function toPrimitive(t, r) {
66
66
  return ("string" === r ? String : Number)(t);
67
67
  }
68
68
  //#endregion
69
- //#region \0@oxc-project+runtime@0.132.0/helpers/toPropertyKey.js
69
+ //#region \0@oxc-project+runtime@0.143.0/helpers/esm/toPropertyKey.js
70
70
  function toPropertyKey(t) {
71
71
  var i = toPrimitive(t, "string");
72
72
  return "symbol" == _typeof(i) ? i : i + "";
73
73
  }
74
74
  //#endregion
75
- //#region \0@oxc-project+runtime@0.132.0/helpers/defineProperty.js
75
+ //#region \0@oxc-project+runtime@0.143.0/helpers/esm/defineProperty.js
76
76
  function _defineProperty(e, r, t) {
77
77
  return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
78
78
  value: t,
@@ -133,7 +133,8 @@ var createIsCacheItemFresh = (loaderMap) => ({ routeItem, pathname }) => {
133
133
  * LICENSE file in the root directory of this source tree.
134
134
  */
135
135
  var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((exports) => {
136
- var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
136
+ var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element");
137
+ var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
137
138
  function jsxProd(type, config, maybeKey) {
138
139
  var key = null;
139
140
  void 0 !== maybeKey && (key = "" + maybeKey);
@@ -756,7 +757,7 @@ var useLocation = () => {
756
757
  };
757
758
  //#endregion
758
759
  //#region components/Link.tsx
759
- var defaultAs = ({ isActive, isPending, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { ...props });
760
+ var defaultAs = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { ...props });
760
761
  var Link = ({ children, to, as = defaultAs, prefetch: prefetchLink, hoverPrefetchDelay, className, style, beforeNavigate, exact = false, activeClassName = "active-link", pendingClassName = "pending-link" }) => {
761
762
  const isPending = useIsRoutePending(to);
762
763
  const { pathname } = useLocation();
@@ -792,7 +793,8 @@ var Link = ({ children, to, as = defaultAs, prefetch: prefetchLink, hoverPrefetc
792
793
  if (prefetch !== "viewport") return;
793
794
  const element = elementRef.current;
794
795
  if (!element) return;
795
- const observer = new IntersectionObserver(async () => {
796
+ const observer = new IntersectionObserver(async ([entry]) => {
797
+ if (!entry.isIntersecting) return;
796
798
  await router.runtime.prefetch(to);
797
799
  observer.disconnect();
798
800
  });
@@ -830,9 +832,10 @@ var Link = ({ children, to, as = defaultAs, prefetch: prefetchLink, hoverPrefetc
830
832
  onClick: clickHandler,
831
833
  onMouseEnter,
832
834
  onMouseLeave,
833
- isActive,
834
- isPending,
835
835
  children
836
+ }, {
837
+ isActive,
838
+ isPending
836
839
  });
837
840
  };
838
841
  //#endregion
@@ -990,8 +993,10 @@ var useSearchParams = () => {
990
993
  currentParams.delete(param);
991
994
  (Array.isArray(value) ? value : [value]).forEach((v) => currentParams.append(param, v));
992
995
  navigateWithSearchParams(currentParams);
993
- } else if (typeof param === "function") navigateWithSearchParams(param(currentParams));
994
- else throw new Error("useSearchParams first argument must be either function or string");
996
+ } else if (typeof param === "function") {
997
+ const newParams = param(currentParams);
998
+ navigateWithSearchParams(newParams);
999
+ } else throw new Error("useSearchParams first argument must be either function or string");
995
1000
  }, [navigateWithSearchParams, search])
996
1001
  };
997
1002
  };
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ComponentType, Dispatch, ReactElement, ReactNode, SetStateAction } from 'react';
1
+ import { ComponentType, type CSSProperties, Dispatch, MouseEvent, ReactElement, ReactNode, Ref, SetStateAction } from 'react';
2
2
  import { Store, useGlobalState } from './create';
3
3
  import { Cell } from './cell';
4
4
  export type LazyComponent = () => Promise<{
@@ -154,4 +154,14 @@ export type InvalidateResult = {
154
154
  path: string;
155
155
  data: unknown;
156
156
  };
157
+ export type ElementProps<T extends HTMLElement = HTMLElement> = {
158
+ ref: Ref<T>;
159
+ href: string;
160
+ className?: string;
161
+ style?: CSSProperties;
162
+ onClick(event: MouseEvent): void;
163
+ onMouseEnter(event: MouseEvent): void;
164
+ onMouseLeave(event: MouseEvent): void;
165
+ children?: ReactNode;
166
+ };
157
167
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.9.4",
3
+ "version": "1.9.6",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {