clear-react-router 1.3.5 → 1.3.7

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
@@ -13,6 +13,7 @@ A lightweight, type-safe routing library for React applications with nested rout
13
13
  - 📦 **Prefetching** - Preload data on hover for instant navigation
14
14
  - 🚀 **Lazy Loading** - Code-split your routes with dynamic imports for optimal performance
15
15
  - 📍 **Scroll Restoration** — Automatically saves and restores scroll position when navigating back to a page (preserves user's scroll position)
16
+ - 🔍 **Typed Query Param** — Type-safe reading and writing of URL query parameters with built-in parsers for strings, numbers, booleans, arrays, and Zod schemas
16
17
  - 🎨 **Flexible API** - Use components or hooks as you prefer
17
18
  - 📱 **Browser History** - Full support for browser back/forward buttons
18
19
  - 🧠 **Context-aware** - Pass and update context through routes
@@ -87,9 +88,21 @@ Component for client-side navigation with prefetch support.
87
88
  | Prop | Type | Default |
88
89
  |------|------|---------|
89
90
  | `to` | `string` | required |
90
- | `prefetch` | `boolean` | `true` |
91
+ | `prefetch` | `boolean` optional | `true` |
92
+ | `prefetchDelay` | `number` optional | `150` | Delay in ms before prefetch starts (prevents unnecessary requests on quick mouse passes) |
91
93
  | `children` | `ReactElement` | required |
92
94
 
95
+ ```
96
+ // Default — prefetch after 150ms hover
97
+ <Link to="/dashboard">Dashboard</Link>
98
+
99
+ // Custom prefetch delay (300ms)
100
+ <Link to="/heavy-page" prefetchDelay={300}>Heavy Page</Link>
101
+
102
+ // Disable prefetch
103
+ <Link to="/about" prefetch={false}>About</Link>
104
+ ```
105
+
93
106
  ### `redirect`
94
107
 
95
108
  Function provided to `beforeLoad` for programmatic redirection.
@@ -279,6 +292,114 @@ useBeforeUnload(text ? onSave : undefined);
279
292
  ```
280
293
  > **Note:** Pass `undefined` to disable the handler (e.g., if there is no changes).
281
294
 
295
+ ### `useQueryParam()`
296
+
297
+ A flexible hook for working with typed query parameters. You provide a parser function, and it returns the parsed value and a setter.
298
+
299
+ ```
300
+ import { useQueryParam, parser } from 'clear-react-router';
301
+
302
+ const ProductPage = () => {
303
+ // String parameter
304
+ const [brand, setBrand] = useQueryParam('brand', parser.string, 'nike');
305
+
306
+ // Number parameter
307
+ const [page, setPage] = useQueryParam('page', parser.integer, 1);
308
+
309
+ // Boolean parameter
310
+ const [isActive, setIsActive] = useQueryParam('active', parser.boolean, false);
311
+
312
+ // Array of strings
313
+ const [colors, setColors] = useQueryParam('colors', parser.stringArray);
314
+
315
+ // Array of numbers
316
+ const [prices, setPrices] = useQueryParam('prices', parser.floatArray);
317
+
318
+ return (
319
+ <div>
320
+ <p>Brand: {brand}</p>
321
+ <p>Page: {page}</p>
322
+ <button onClick={() => setPage(page + 1)}>Next</button>
323
+ </div>
324
+ );
325
+ }
326
+ ```
327
+ **Signature:** `useQueryParam<T>(field: string, parser: (arg: string[]) => T, defaultValue?: T): [T, (arg: T) => void]`
328
+
329
+ | Argument | Type | Description |
330
+ |----------|------|-------------|
331
+ | `field` | `string` | The query parameter key (e.g., `'page'`, `'brand'`) |
332
+ | `parser` | `(arg: string[]) => T` | Function that transforms the raw string array into the desired type |
333
+ | `defaultValue` | `T` (optional) | Default value returned when the parameter is missing or empty |
334
+
335
+ **Returns:**
336
+
337
+ | Element | Type | Description |
338
+ |---------|------|-------------|
339
+ | `value` | `T` | The parsed value from the query parameter |
340
+ | `setValue` | `(arg: T) => void` | Function to update the query parameter |
341
+
342
+ ### Built-in Parsers
343
+
344
+ | Parser | Input | Output | Description |
345
+ |--------|-------|--------|-------------|
346
+ | `parser.string` | `string[]` | `string` | First value or empty string |
347
+ | `parser.stringArray` | `string[]` | `string[]` | All values as array |
348
+ | `parser.integer` | `string[]` | `number` | First value parsed as integer (default: `0`) |
349
+ | `parser.integerArray` | `string[]` | `number[]` | All values parsed as integers |
350
+ | `parser.float` | `string[]` | `number` | First value parsed as float (default: `0`) |
351
+ | `parser.floatArray` | `string[]` | `number[]` | All values parsed as floats |
352
+ | `parser.boolean` | `string[]` | `boolean` | First value parsed as boolean (`'true'` → `true`) |
353
+ | `parser.booleanArray` | `string[]` | `boolean[]` | All values parsed as booleans |
354
+
355
+ ### Using Zod Schemas
356
+ `useQueryParam` works seamlessly with Zod for complex validation:
357
+
358
+ ```
359
+ import { z } from 'zod';
360
+ import { useQueryParam, parser } from 'clear-react-router';
361
+
362
+ const filterSchema = z.object({
363
+ name: z.string(),
364
+ age: z.number().min(0),
365
+ active: z.boolean().optional(),
366
+ });
367
+
368
+ function ProductFilter() {
369
+ const [filter, setFilter] = useQueryParam(
370
+ 'filter',
371
+ parser.zodSchema(filterSchema),
372
+ { name: '', age: 0 }
373
+ );
374
+
375
+ return (
376
+ <div>
377
+ <p>Name: {filter.name}</p>
378
+ <p>Age: {filter.age}</p>
379
+ <button onClick={() => setFilter({ ...filter, age: filter.age + 1 })}>
380
+ Increment Age
381
+ </button>
382
+ </div>
383
+ );
384
+ }
385
+ ```
386
+
387
+ ### Custom Parsers
388
+ You can write your own parser for any format:
389
+
390
+ ```
391
+ // Custom parser for comma-separated values
392
+ const csvParser = (params: string[]): string[] => {
393
+ const value = params[0] || '';
394
+ return value ? value.split(',').map(v => v.trim()) : [];
395
+ };
396
+
397
+ const TagsFilter() {
398
+ const [tags, setTags] = useQueryParam('tags', csvParser, []);
399
+ // tags: string[]
400
+ }
401
+ ```
402
+
282
403
  ### `useRouterContext()`
283
404
 
284
405
  Returns the router context object and a function to update it. Useful for accessing or modifying global state (like user authentication, theme, etc.) from anywhere in your app.
@@ -6,6 +6,7 @@ type LinkProps = {
6
6
  style: CSSProperties;
7
7
  }>;
8
8
  prefetch?: boolean;
9
+ prefetchDelay?: number;
9
10
  };
10
- export declare const Link: ({ children, to, prefetch }: LinkProps) => import("react/jsx-runtime").JSX.Element;
11
+ export declare const Link: ({ children, to, prefetch, prefetchDelay }: LinkProps) => import("react/jsx-runtime").JSX.Element;
11
12
  export {};
@@ -1,3 +1,4 @@
1
+ import { Dispatch, SetStateAction } from 'react';
1
2
  import { BlockerState, Location, RouteItem, UpdateBlockedRouteProps } from '../types/global';
2
3
  export type PropsContextValue = {
3
4
  routeList: RouteItem[];
@@ -10,6 +11,7 @@ export type NavigationContextValue = {
10
11
  shouldErrorElementShown: boolean;
11
12
  };
12
13
  export type ActionsContextValue = {
14
+ setLocation: Dispatch<SetStateAction<Location>>;
13
15
  updateLocation(route: Location): Promise<void>;
14
16
  updateBlockedRoute(arg: UpdateBlockedRouteProps): void;
15
17
  prefetchLoader(arg: string): Promise<void>;
@@ -0,0 +1 @@
1
+ export declare function useQueryParam<T>(field: string, parser: (arg: string[]) => T, defaultValue?: T): [T, (arg: T) => void];
package/dist/index.d.ts CHANGED
@@ -8,7 +8,9 @@ export { useLoaderState } from './hooks/useLoaderState';
8
8
  export { useBlocker } from './hooks/useBlocker';
9
9
  export { useBeforeUnload } from './hooks/useBeforeUnload';
10
10
  export { useRouterContext } from './hooks/useRouterContext';
11
+ export { useQueryParam } from './hooks/useQueryParam';
11
12
  export { useSearchParams } from './hooks/useSearchParams';
12
13
  export { useHistoricalTrail } from './hooks/useHistoricalTrail';
14
+ export { parser } from './utils/parser';
13
15
  export { createRouter } from './utils/utils';
14
16
  export type { RouteItem, BlockerState, Location } from './types/global';
package/dist/index.js CHANGED
@@ -45,7 +45,7 @@ var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((expor
45
45
  var import_jsx_runtime = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
46
46
  module.exports = require_react_jsx_runtime_production();
47
47
  })))();
48
- var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocation, location, prefetchLoader, loaderCache, blockerState, routeList, shouldErrorElementShown, isLoading, isAnimated }) => {
48
+ var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocation, location, setLocation, prefetchLoader, loaderCache, blockerState, routeList, shouldErrorElementShown, isLoading, isAnimated }) => {
49
49
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PropsContext.Provider, {
50
50
  value: {
51
51
  routeList,
@@ -53,6 +53,7 @@ var Provider = ({ children, setContext, context, updateBlockedRoute, updateLocat
53
53
  },
54
54
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActionsContext.Provider, {
55
55
  value: {
56
+ setLocation,
56
57
  updateLocation,
57
58
  updateBlockedRoute,
58
59
  prefetchLoader,
@@ -431,6 +432,7 @@ var RouterProvider = ({ children, routeList, context: initialContext = {}, isAni
431
432
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Provider, {
432
433
  ...useMemo(() => ({
433
434
  location,
435
+ setLocation,
434
436
  updateLocation,
435
437
  loaderCache,
436
438
  prefetchLoader,
@@ -543,13 +545,31 @@ var useNavigate = () => {
543
545
  };
544
546
  //#endregion
545
547
  //#region components/Link.tsx
546
- var Link = ({ children, to, prefetch = true }) => {
548
+ var STANDARD_DELAY = 150;
549
+ var Link = ({ children, to, prefetch = true, prefetchDelay = STANDARD_DELAY }) => {
547
550
  const { prefetchLoader } = useRouterActions();
548
551
  const navigate = useNavigate();
552
+ const timeout = useRef(0);
549
553
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
550
554
  style: { cursor: "pointer" },
551
555
  onClick: () => navigate(to),
552
- onMouseOver: () => prefetch && prefetchLoader(to),
556
+ onMouseEnter: useCallback(() => {
557
+ if (!prefetch || !prefetchDelay) return;
558
+ if (timeout.current) clearTimeout(timeout.current);
559
+ timeout.current = window.setTimeout(() => prefetchLoader(to), prefetchDelay);
560
+ }, [
561
+ prefetch,
562
+ prefetchDelay,
563
+ prefetchLoader,
564
+ to
565
+ ]),
566
+ onMouseLeave: useCallback(() => {
567
+ if (!prefetch || !prefetchDelay) return;
568
+ if (timeout.current) {
569
+ clearTimeout(timeout.current);
570
+ timeout.current = 0;
571
+ }
572
+ }, [prefetch, prefetchDelay]),
553
573
  children
554
574
  });
555
575
  };
@@ -611,7 +631,7 @@ var useRouterContext = () => {
611
631
  //#region hooks/useSearchParams.ts
612
632
  var useSearchParams = () => {
613
633
  const location = useLocation();
614
- const navigate = useNavigate();
634
+ const { setLocation } = useRouterActions();
615
635
  const locationRef = useLatest(location);
616
636
  const searchString = useMemo(() => location.search ? location.search.replace("?", "") : location.pathname.split("?")?.[1] ?? "", [location.pathname, location.search]);
617
637
  const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
@@ -621,11 +641,14 @@ var useSearchParams = () => {
621
641
  }, [searchParams]);
622
642
  const navigateWithSearchParams = useCallback((params) => {
623
643
  const newSearch = params.toString();
624
- navigate({
625
- pathname: locationRef.current.pathname,
626
- search: newSearch ? `?${newSearch}` : ""
627
- });
628
- }, [locationRef, navigate]);
644
+ const { pathname } = locationRef.current;
645
+ const search = newSearch ? `?${newSearch}` : "";
646
+ setLocation((prevState) => ({
647
+ ...prevState,
648
+ search
649
+ }));
650
+ history.replaceState(null, "", pathname + search);
651
+ }, [locationRef, setLocation]);
629
652
  return {
630
653
  searchParams,
631
654
  getSearchParams,
@@ -642,6 +665,24 @@ var useSearchParams = () => {
642
665
  };
643
666
  };
644
667
  //#endregion
668
+ //#region hooks/useQueryParam.ts
669
+ function useQueryParam(field, parser, defaultValue) {
670
+ const { searchParams, setSearchParams } = useSearchParams();
671
+ return [useMemo(() => {
672
+ const result = parser(searchParams.getAll(field));
673
+ if (result !== void 0 && result !== null && result !== "") return result;
674
+ if (defaultValue !== void 0) return defaultValue;
675
+ return result;
676
+ }, [
677
+ field,
678
+ parser,
679
+ searchParams,
680
+ defaultValue
681
+ ]), useCallback((value) => {
682
+ setSearchParams(field, (typeof value === "object" ? JSON.stringify : String)(value));
683
+ }, [setSearchParams, field])];
684
+ }
685
+ //#endregion
645
686
  //#region hooks/useHistoricalTrail.ts
646
687
  var useHistoricalTrail = () => {
647
688
  const { pathname } = useLocation();
@@ -656,4 +697,39 @@ var useHistoricalTrail = () => {
656
697
  return trail;
657
698
  };
658
699
  //#endregion
659
- export { Link, Router, RouterProvider, createRouter, useBeforeUnload, useBlocker, useHistoricalTrail, useLoaderState, useLocation, useNavigate, useParams, useRouterContext, useSearchParams };
700
+ //#region utils/parser.ts
701
+ var parser = {
702
+ string: (params) => params[0] || "",
703
+ stringArray: (params) => params,
704
+ integer: (params) => {
705
+ const result = parseInt(params[0] || "", 10);
706
+ return isNaN(result) ? 0 : result;
707
+ },
708
+ integerArray: (params) => params.map((el) => {
709
+ const result = parseInt(el, 10);
710
+ return isNaN(result) ? 0 : result;
711
+ }),
712
+ float: (params) => {
713
+ const result = parseFloat(params[0] || "");
714
+ return isNaN(result) ? 0 : result;
715
+ },
716
+ floatArray: (params) => params.map((el) => {
717
+ const result = parseFloat(el);
718
+ return isNaN(result) ? 0 : result;
719
+ }),
720
+ boolean: (params) => params[0]?.toLowerCase() === "true",
721
+ booleanArray: (params) => params.map((el) => el.toLowerCase() === "true"),
722
+ zodSchema: (schema) => (params) => {
723
+ let parsed;
724
+ try {
725
+ parsed = JSON.parse(params[0] ?? "{}");
726
+ } catch {
727
+ throw new Error("Invalid JSON");
728
+ }
729
+ const result = schema.safeParse(parsed);
730
+ if (!result.success) throw new Error("Invalid schema");
731
+ return result.data;
732
+ }
733
+ };
734
+ //#endregion
735
+ export { Link, Router, RouterProvider, createRouter, parser, useBeforeUnload, useBlocker, useHistoricalTrail, useLoaderState, useLocation, useNavigate, useParams, useQueryParam, useRouterContext, useSearchParams };
@@ -3,5 +3,5 @@ import { type ActionsContextValue, type DataContextValue, type NavigationContext
3
3
  type ProviderProps = PropsContextValue & NavigationContextValue & ActionsContextValue & DataContextValue & {
4
4
  children: ReactNode;
5
5
  };
6
- export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation, location, prefetchLoader, loaderCache, blockerState, routeList, shouldErrorElementShown, isLoading, isAnimated, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
6
+ export declare const Provider: ({ children, setContext, context, updateBlockedRoute, updateLocation, location, setLocation, prefetchLoader, loaderCache, blockerState, routeList, shouldErrorElementShown, isLoading, isAnimated, }: ProviderProps) => import("react/jsx-runtime").JSX.Element;
7
7
  export {};
@@ -0,0 +1,21 @@
1
+ type ZodInterface<T> = {
2
+ safeParse(input: unknown): {
3
+ success: true;
4
+ data: T;
5
+ } | {
6
+ success: false;
7
+ error: unknown;
8
+ };
9
+ };
10
+ export declare const parser: {
11
+ string: (params: string[]) => string;
12
+ stringArray: (params: string[]) => string[];
13
+ integer: (params: string[]) => number;
14
+ integerArray: (params: string[]) => number[];
15
+ float: (params: string[]) => number;
16
+ floatArray: (params: string[]) => number[];
17
+ boolean: (params: string[]) => boolean;
18
+ booleanArray: (params: string[]) => boolean[];
19
+ zodSchema: <T>(schema: ZodInterface<T>) => (params: string[]) => T;
20
+ };
21
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {