clear-react-router 1.3.4 → 1.3.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 +125 -4
- package/dist/context/RouterProviderContext.d.ts +2 -0
- package/dist/hooks/useHistoricalTrail.d.ts +1 -0
- package/dist/hooks/useQueryParam.d.ts +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +80 -8
- package/dist/provider/Provider.d.ts +1 -1
- package/dist/utils/parser.d.ts +21 -0
- package/package.json +11 -2
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
|
|
@@ -279,6 +280,114 @@ useBeforeUnload(text ? onSave : undefined);
|
|
|
279
280
|
```
|
|
280
281
|
> **Note:** Pass `undefined` to disable the handler (e.g., if there is no changes).
|
|
281
282
|
|
|
283
|
+
### `useQueryParam()`
|
|
284
|
+
|
|
285
|
+
A flexible hook for working with typed query parameters. You provide a parser function, and it returns the parsed value and a setter.
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
import { useQueryParam, parser } from 'clear-react-router';
|
|
289
|
+
|
|
290
|
+
const ProductPage = () => {
|
|
291
|
+
// String parameter
|
|
292
|
+
const [brand, setBrand] = useQueryParam('brand', parser.string, 'nike');
|
|
293
|
+
|
|
294
|
+
// Number parameter
|
|
295
|
+
const [page, setPage] = useQueryParam('page', parser.integer, 1);
|
|
296
|
+
|
|
297
|
+
// Boolean parameter
|
|
298
|
+
const [isActive, setIsActive] = useQueryParam('active', parser.boolean, false);
|
|
299
|
+
|
|
300
|
+
// Array of strings
|
|
301
|
+
const [colors, setColors] = useQueryParam('colors', parser.stringArray);
|
|
302
|
+
|
|
303
|
+
// Array of numbers
|
|
304
|
+
const [prices, setPrices] = useQueryParam('prices', parser.floatArray);
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div>
|
|
308
|
+
<p>Brand: {brand}</p>
|
|
309
|
+
<p>Page: {page}</p>
|
|
310
|
+
<button onClick={() => setPage(page + 1)}>Next</button>
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
**Signature:** `useQueryParam<T>(field: string, parser: (arg: string[]) => T, defaultValue?: T): [T, (arg: T) => void]`
|
|
316
|
+
|
|
317
|
+
| Argument | Type | Description |
|
|
318
|
+
|----------|------|-------------|
|
|
319
|
+
| `field` | `string` | The query parameter key (e.g., `'page'`, `'brand'`) |
|
|
320
|
+
| `parser` | `(arg: string[]) => T` | Function that transforms the raw string array into the desired type |
|
|
321
|
+
| `defaultValue` | `T` (optional) | Default value returned when the parameter is missing or empty |
|
|
322
|
+
|
|
323
|
+
**Returns:**
|
|
324
|
+
|
|
325
|
+
| Element | Type | Description |
|
|
326
|
+
|---------|------|-------------|
|
|
327
|
+
| `value` | `T` | The parsed value from the query parameter |
|
|
328
|
+
| `setValue` | `(arg: T) => void` | Function to update the query parameter |
|
|
329
|
+
|
|
330
|
+
### Built-in Parsers
|
|
331
|
+
|
|
332
|
+
| Parser | Input | Output | Description |
|
|
333
|
+
|--------|-------|--------|-------------|
|
|
334
|
+
| `parser.string` | `string[]` | `string` | First value or empty string |
|
|
335
|
+
| `parser.stringArray` | `string[]` | `string[]` | All values as array |
|
|
336
|
+
| `parser.integer` | `string[]` | `number` | First value parsed as integer (default: `0`) |
|
|
337
|
+
| `parser.integerArray` | `string[]` | `number[]` | All values parsed as integers |
|
|
338
|
+
| `parser.float` | `string[]` | `number` | First value parsed as float (default: `0`) |
|
|
339
|
+
| `parser.floatArray` | `string[]` | `number[]` | All values parsed as floats |
|
|
340
|
+
| `parser.boolean` | `string[]` | `boolean` | First value parsed as boolean (`'true'` → `true`) |
|
|
341
|
+
| `parser.booleanArray` | `string[]` | `boolean[]` | All values parsed as booleans |
|
|
342
|
+
|
|
343
|
+
### Using Zod Schemas
|
|
344
|
+
`useQueryParam` works seamlessly with Zod for complex validation:
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
import { z } from 'zod';
|
|
348
|
+
import { useQueryParam, parser } from 'clear-react-router';
|
|
349
|
+
|
|
350
|
+
const filterSchema = z.object({
|
|
351
|
+
name: z.string(),
|
|
352
|
+
age: z.number().min(0),
|
|
353
|
+
active: z.boolean().optional(),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
function ProductFilter() {
|
|
357
|
+
const [filter, setFilter] = useQueryParam(
|
|
358
|
+
'filter',
|
|
359
|
+
parser.zodSchema(filterSchema),
|
|
360
|
+
{ name: '', age: 0 }
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<div>
|
|
365
|
+
<p>Name: {filter.name}</p>
|
|
366
|
+
<p>Age: {filter.age}</p>
|
|
367
|
+
<button onClick={() => setFilter({ ...filter, age: filter.age + 1 })}>
|
|
368
|
+
Increment Age
|
|
369
|
+
</button>
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Custom Parsers
|
|
376
|
+
You can write your own parser for any format:
|
|
377
|
+
|
|
378
|
+
```
|
|
379
|
+
// Custom parser for comma-separated values
|
|
380
|
+
const csvParser = (params: string[]): string[] => {
|
|
381
|
+
const value = params[0] || '';
|
|
382
|
+
return value ? value.split(',').map(v => v.trim()) : [];
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const TagsFilter() {
|
|
386
|
+
const [tags, setTags] = useQueryParam('tags', csvParser, []);
|
|
387
|
+
// tags: string[]
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
282
391
|
### `useRouterContext()`
|
|
283
392
|
|
|
284
393
|
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.
|
|
@@ -327,13 +436,25 @@ function ProductFilter() {
|
|
|
327
436
|
|
|
328
437
|
**Key features:**
|
|
329
438
|
|
|
330
|
-
-
|
|
331
|
-
-
|
|
332
|
-
-
|
|
333
|
-
-
|
|
439
|
+
- **Array support** — `getSearchParams` returns `string[]` when multiple values exist for the same key
|
|
440
|
+
- **Functional updates** — Update parameters based on previous state without losing other params
|
|
441
|
+
- **Type-safe** — Proper TypeScript support with overloads
|
|
442
|
+
- **Stable reference** — `setSearchParams` reference is stable and safe to use in `useEffect`
|
|
334
443
|
|
|
335
444
|
> **Note:** `getSearchParams` returns `string` for single values, `string[]` for multiple values, and `''` if the key is not found.
|
|
336
445
|
|
|
446
|
+
### `useHistoricalTrail()`
|
|
447
|
+
|
|
448
|
+
Returns an array of pathnames representing the user's actual navigation history. Perfect for **history-based breadcrumbs** in dashboards, admin panels, multi-step forms, or any app where users navigate non-linearly.
|
|
449
|
+
|
|
450
|
+
**Returns:** Array of pathnames in chronological visit order (e.g., `['/dashboard', '/users', '/settings']`)
|
|
451
|
+
|
|
452
|
+
**Key features:**
|
|
453
|
+
- **Chronological order** — Paths are stored in the order the user visited them
|
|
454
|
+
- **Unique entries** — Revisiting a page trims the trail to that point
|
|
455
|
+
- **Respects navigation blocking** — Only successful navigations are added
|
|
456
|
+
- **Redirect-safe** — Redirected pages are not added to the trail
|
|
457
|
+
|
|
337
458
|
## Lazy Loading
|
|
338
459
|
|
|
339
460
|
Clear Router supports code-splitting out of the box. Simply pass a function that returns a dynamic import:
|
|
@@ -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 const useHistoricalTrail: () => string[];
|
|
@@ -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,6 +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';
|
|
13
|
+
export { useHistoricalTrail } from './hooks/useHistoricalTrail';
|
|
14
|
+
export { parser } from './utils/parser';
|
|
12
15
|
export { createRouter } from './utils/utils';
|
|
13
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,
|
|
@@ -611,7 +613,7 @@ var useRouterContext = () => {
|
|
|
611
613
|
//#region hooks/useSearchParams.ts
|
|
612
614
|
var useSearchParams = () => {
|
|
613
615
|
const location = useLocation();
|
|
614
|
-
const
|
|
616
|
+
const { setLocation } = useRouterActions();
|
|
615
617
|
const locationRef = useLatest(location);
|
|
616
618
|
const searchString = useMemo(() => location.search ? location.search.replace("?", "") : location.pathname.split("?")?.[1] ?? "", [location.pathname, location.search]);
|
|
617
619
|
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
|
|
@@ -621,11 +623,14 @@ var useSearchParams = () => {
|
|
|
621
623
|
}, [searchParams]);
|
|
622
624
|
const navigateWithSearchParams = useCallback((params) => {
|
|
623
625
|
const newSearch = params.toString();
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
626
|
+
const { pathname } = locationRef.current;
|
|
627
|
+
const search = newSearch ? `?${newSearch}` : "";
|
|
628
|
+
setLocation((prevState) => ({
|
|
629
|
+
...prevState,
|
|
630
|
+
search
|
|
631
|
+
}));
|
|
632
|
+
history.replaceState(null, "", pathname + search);
|
|
633
|
+
}, [locationRef, setLocation]);
|
|
629
634
|
return {
|
|
630
635
|
searchParams,
|
|
631
636
|
getSearchParams,
|
|
@@ -642,4 +647,71 @@ var useSearchParams = () => {
|
|
|
642
647
|
};
|
|
643
648
|
};
|
|
644
649
|
//#endregion
|
|
645
|
-
|
|
650
|
+
//#region hooks/useQueryParam.ts
|
|
651
|
+
function useQueryParam(field, parser, defaultValue) {
|
|
652
|
+
const { searchParams, setSearchParams } = useSearchParams();
|
|
653
|
+
return [useMemo(() => {
|
|
654
|
+
const result = parser(searchParams.getAll(field));
|
|
655
|
+
if (result !== void 0 && result !== null && result !== "") return result;
|
|
656
|
+
if (defaultValue !== void 0) return defaultValue;
|
|
657
|
+
return result;
|
|
658
|
+
}, [
|
|
659
|
+
field,
|
|
660
|
+
parser,
|
|
661
|
+
searchParams,
|
|
662
|
+
defaultValue
|
|
663
|
+
]), useCallback((value) => {
|
|
664
|
+
setSearchParams(field, (typeof value === "object" ? JSON.stringify : String)(value));
|
|
665
|
+
}, [setSearchParams, field])];
|
|
666
|
+
}
|
|
667
|
+
//#endregion
|
|
668
|
+
//#region hooks/useHistoricalTrail.ts
|
|
669
|
+
var useHistoricalTrail = () => {
|
|
670
|
+
const { pathname } = useLocation();
|
|
671
|
+
const [trail, setTrail] = useState([]);
|
|
672
|
+
useEffect(() => {
|
|
673
|
+
if (!pathname) return;
|
|
674
|
+
setTrail((prevState) => {
|
|
675
|
+
const index = prevState.indexOf(pathname);
|
|
676
|
+
return index === -1 ? [...prevState, pathname] : prevState.slice(0, index + 1);
|
|
677
|
+
});
|
|
678
|
+
}, [pathname]);
|
|
679
|
+
return trail;
|
|
680
|
+
};
|
|
681
|
+
//#endregion
|
|
682
|
+
//#region utils/parser.ts
|
|
683
|
+
var parser = {
|
|
684
|
+
string: (params) => params[0] || "",
|
|
685
|
+
stringArray: (params) => params,
|
|
686
|
+
integer: (params) => {
|
|
687
|
+
const result = parseInt(params[0] || "", 10);
|
|
688
|
+
return isNaN(result) ? 0 : result;
|
|
689
|
+
},
|
|
690
|
+
integerArray: (params) => params.map((el) => {
|
|
691
|
+
const result = parseInt(el, 10);
|
|
692
|
+
return isNaN(result) ? 0 : result;
|
|
693
|
+
}),
|
|
694
|
+
float: (params) => {
|
|
695
|
+
const result = parseFloat(params[0] || "");
|
|
696
|
+
return isNaN(result) ? 0 : result;
|
|
697
|
+
},
|
|
698
|
+
floatArray: (params) => params.map((el) => {
|
|
699
|
+
const result = parseFloat(el);
|
|
700
|
+
return isNaN(result) ? 0 : result;
|
|
701
|
+
}),
|
|
702
|
+
boolean: (params) => params[0]?.toLowerCase() === "true",
|
|
703
|
+
booleanArray: (params) => params.map((el) => el.toLowerCase() === "true"),
|
|
704
|
+
zodSchema: (schema) => (params) => {
|
|
705
|
+
let parsed;
|
|
706
|
+
try {
|
|
707
|
+
parsed = JSON.parse(params[0] ?? "{}");
|
|
708
|
+
} catch {
|
|
709
|
+
throw new Error("Invalid JSON");
|
|
710
|
+
}
|
|
711
|
+
const result = schema.safeParse(parsed);
|
|
712
|
+
if (!result.success) throw new Error("Invalid schema");
|
|
713
|
+
return result.data;
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
//#endregion
|
|
717
|
+
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.
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"description": "A lightweight, type-safe routing library for React applications",
|
|
5
5
|
"author": "Andrew Bubnov",
|
|
6
6
|
"scripts": {
|
|
@@ -12,7 +12,16 @@
|
|
|
12
12
|
"routing",
|
|
13
13
|
"typescript",
|
|
14
14
|
"spa",
|
|
15
|
-
"nested-routes"
|
|
15
|
+
"nested-routes",
|
|
16
|
+
"data-loading",
|
|
17
|
+
"navigation-blocker",
|
|
18
|
+
"scroll-restoration",
|
|
19
|
+
"breadcrumbs",
|
|
20
|
+
"search-params",
|
|
21
|
+
"lazy-loading",
|
|
22
|
+
"prefetching",
|
|
23
|
+
"view-transitions",
|
|
24
|
+
"animations"
|
|
16
25
|
],
|
|
17
26
|
"repository": {
|
|
18
27
|
"type": "git",
|