clear-react-router 1.3.9 → 1.4.1

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
@@ -224,13 +224,21 @@ const { pathname, search, state } = useLocation();
224
224
 
225
225
  ### `useLoaderState()`
226
226
 
227
- Returns the cached data loaded by the current route's `loader`. Data is automatically cached and reused when navigating back to the same route.
227
+ Returns the cached data loaded by the current route's `loader`, along with any errors from `loader` or `beforeLoad`. Data is automatically cached and reused when navigating back to the same route.
228
+
229
+ **Returns:**
230
+
231
+ | Property | Type | Description |
232
+ |----------|------|-------------|
233
+ | `data` | `unknown` | The data returned from the route's `loader` |
234
+ | `loaderError` | `Error \| null` | Error from the `loader` (if any) |
235
+ | `beforeLoadError` | `Error \| null` | Error from the `beforeLoad` hook (if any) |
236
+
228
237
  ```
229
- const UserProfile = () => {
230
- const user = useLoaderState();
231
- return <div>{user.name}</div>;
232
- }
238
+ function UserProfile() {
239
+ const { data, loaderError, beforeLoadError } = useLoaderState();
233
240
  ```
241
+
234
242
  ### Caching behavior:
235
243
  - The loader result is cached and reused when navigating back to the same route (e.g., from /user/123 back to /user/456 it will be a new request because different params, but from /user/456 to /user/456 — cache hit).
236
244
  - Use staleTime in route config to control how long cache is considered fresh:
@@ -294,26 +302,23 @@ useBeforeUnload(text ? onSave : undefined);
294
302
 
295
303
  ### `useQueryParam()`
296
304
 
297
- A flexible hook for working with typed query parameters. You provide a parser function, and it returns the parsed value and a setter.
305
+ A flexible hook for working with typed query parameters. You provide an adapter object with `parse` and `serialize` functions, and it returns the parsed value and a setter.
298
306
 
299
307
  ```
300
- import { useQueryParam, parser } from 'clear-react-router';
308
+ import { useQueryParam, adapter } from 'clear-react-router';
301
309
 
302
310
  const ProductPage = () => {
303
311
  // String parameter
304
- const [brand, setBrand] = useQueryParam('brand', parser.string, 'nike');
312
+ const [brand, setBrand] = useQueryParam('brand', adapter.string, 'nike');
305
313
 
306
314
  // Number parameter
307
- const [page, setPage] = useQueryParam('page', parser.integer, 1);
315
+ const [page, setPage] = useQueryParam('page', adapter.integer, 1);
308
316
 
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);
317
+ // Date parameter
318
+ const [date, setDate] = useQueryParam('date', adapter.date, new Date());
314
319
 
315
320
  // Array of numbers
316
- const [prices, setPrices] = useQueryParam('prices', parser.floatArray);
321
+ const [prices, setPrices] = useQueryParam('prices', adapter.floatArray);
317
322
 
318
323
  return (
319
324
  <div>
@@ -324,12 +329,17 @@ const ProductPage = () => {
324
329
  );
325
330
  }
326
331
  ```
327
- **Signature:** `useQueryParam<T>(field: string, parser: (arg: string[]) => T, defaultValue?: T): [T, (arg: T) => void]`
332
+ type Adapter<T> = {
333
+ parse: (params: string[]) => T;
334
+ serialize?: (params: T) => string | string[];
335
+ }
336
+
337
+ **Signature:** `useQueryParam<T>(field: string, adapter: Adapter<T>, defaultValue?: T): [T, (arg: T | null) => void]`
328
338
 
329
339
  | Argument | Type | Description |
330
340
  |----------|------|-------------|
331
341
  | `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 |
342
+ | `adapter` | `Adapter<T>` | Parser and optional serializer for params (serializer String is used in case of serializer not passed) |
333
343
  | `defaultValue` | `T` (optional) | Default value returned when the parameter is missing or empty |
334
344
 
335
345
  **Returns:**
@@ -337,27 +347,30 @@ const ProductPage = () => {
337
347
  | Element | Type | Description |
338
348
  |---------|------|-------------|
339
349
  | `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 |
350
+ | `setValue` | `(arg: T | null) => void` | Function to update the query parameter. Null is passed to remove the parameter. |
351
+
352
+ ### Built-in Adapters
353
+
354
+ | Adapter | Input | Output | Description |
355
+ |---------|-------|--------|-------------|
356
+ | `adapter.string` | `string[]` | `string` | First value or empty string |
357
+ | `adapter.stringArray` | `string[]` | `string[]` | All values as array |
358
+ | `adapter.integer` | `string[]` | `number` | First value parsed as integer (default: `0`) |
359
+ | `adapter.integerArray` | `string[]` | `number[]` | All values parsed as integers |
360
+ | `adapter.float` | `string[]` | `number` | First value parsed as float (default: `0`) |
361
+ | `adapter.floatArray` | `string[]` | `number[]` | All values parsed as floats |
362
+ | `adapter.boolean` | `string[]` | `boolean` | First value parsed as boolean (`'true'` → `true`) |
363
+ | `adapter.booleanArray` | `string[]` | `boolean[]` | All values parsed as booleans |
364
+ | `adapter.date` | `string[]` | `Date` | First value parsed as Date from timestamp |
365
+ | `adapter.dateArray` | `string[]` | `Date[]` | All values parsed as Dates from timestamps |
366
+ | `adapter.zodSchema` | `string[]` | `T` | Validates JSON string against Zod schema |
354
367
 
355
368
  ### Using Zod Schemas
356
369
  `useQueryParam` works seamlessly with Zod for complex validation:
357
370
 
358
371
  ```
359
372
  import { z } from 'zod';
360
- import { useQueryParam, parser } from 'clear-react-router';
373
+ import { useQueryParam, adapter } from 'clear-react-router';
361
374
 
362
375
  const filterSchema = z.object({
363
376
  name: z.string(),
@@ -368,7 +381,7 @@ const filterSchema = z.object({
368
381
  function ProductFilter() {
369
382
  const [filter, setFilter] = useQueryParam(
370
383
  'filter',
371
- parser.zodSchema(filterSchema),
384
+ adapter.zodSchema(filterSchema),
372
385
  { name: '', age: 0 }
373
386
  );
374
387
 
@@ -384,18 +397,21 @@ function ProductFilter() {
384
397
  }
385
398
  ```
386
399
 
387
- ### Custom Parsers
388
- You can write your own parser for any format:
400
+ ### Custom Adapters
401
+ You can write your own adapter for any format:
389
402
 
390
403
  ```
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
- };
404
+ // Custom adapter for comma-separated values
405
+ const csvAdapter = {
406
+ parse: (params: string[]): string[] => {
407
+ const value = params[0] || '';
408
+ return value ? value.split(',').map(v => v.trim()) : [];
409
+ },
410
+ serialize: (value: string[]): string[] => value
411
+ }
396
412
 
397
413
  const TagsFilter() {
398
- const [tags, setTags] = useQueryParam('tags', csvParser, []);
414
+ const [tags, setTags] = useQueryParam('tags', csvAdapter, []);
399
415
  // tags: string[]
400
416
  }
401
417
  ```
@@ -1 +1,2 @@
1
- export declare function useQueryParam<T>(field: string, parser: (arg: string[]) => T, defaultValue?: T): [T, (arg: T) => void];
1
+ import { Adapter } from '../types/global';
2
+ export declare function useQueryParam<T>(field: string, adapter: Adapter<T>, defaultValue?: T): [T, (arg: T | null) => void];
package/dist/index.d.ts CHANGED
@@ -11,6 +11,6 @@ export { useRouterContext } from './hooks/useRouterContext';
11
11
  export { useQueryParam } from './hooks/useQueryParam';
12
12
  export { useSearchParams } from './hooks/useSearchParams';
13
13
  export { useHistoricalTrail } from './hooks/useHistoricalTrail';
14
- export { parser } from './utils/parser';
14
+ export { adapter } from './utils/adapter';
15
15
  export { createRouter } from './utils/utils';
16
- export type { RouteItem, BlockerState, Location } from './types/global';
16
+ export type { RouteItem, BlockerState, Location, Adapter } from './types/global';
package/dist/index.js CHANGED
@@ -675,21 +675,28 @@ var useSearchParams = () => {
675
675
  };
676
676
  //#endregion
677
677
  //#region hooks/useQueryParam.ts
678
- function useQueryParam(field, parser, defaultValue) {
678
+ function useQueryParam(field, adapter, defaultValue) {
679
679
  const { searchParams, setSearchParams } = useSearchParams();
680
680
  return [useMemo(() => {
681
- const result = parser(searchParams.getAll(field));
682
- if (result !== void 0 && result !== null && result !== "") return result;
681
+ const params = searchParams.getAll(field);
682
+ const result = adapter.parse(params);
683
+ const isValid = !(result instanceof Date) || result instanceof Date && !isNaN(result.getTime());
684
+ if (result !== void 0 && result !== null && result !== "" && isValid) return result;
683
685
  if (defaultValue !== void 0) return defaultValue;
684
686
  return result;
685
687
  }, [
686
688
  field,
687
- parser,
689
+ adapter,
688
690
  searchParams,
689
691
  defaultValue
690
692
  ]), useCallback((value) => {
691
- setSearchParams(field, (typeof value === "object" ? JSON.stringify : String)(value));
692
- }, [setSearchParams, field])];
693
+ if (!value) return setSearchParams(field, []);
694
+ setSearchParams(field, (adapter.serialize || String)(value));
695
+ }, [
696
+ field,
697
+ adapter.serialize,
698
+ setSearchParams
699
+ ])];
693
700
  }
694
701
  //#endregion
695
702
  //#region hooks/useHistoricalTrail.ts
@@ -706,39 +713,65 @@ var useHistoricalTrail = () => {
706
713
  return trail;
707
714
  };
708
715
  //#endregion
709
- //#region utils/parser.ts
710
- var parser = {
711
- string: (params) => params[0] || "",
712
- stringArray: (params) => params,
713
- integer: (params) => {
714
- const result = parseInt(params[0] || "", 10);
715
- return isNaN(result) ? 0 : result;
716
+ //#region utils/adapter.ts
717
+ var adapter = {
718
+ string: { parse: (params) => params[0] || "" },
719
+ stringArray: {
720
+ parse: (params) => params,
721
+ serialize: (value) => value
716
722
  },
717
- integerArray: (params) => params.map((el) => {
718
- const result = parseInt(el, 10);
723
+ integer: { parse: (params) => {
724
+ const result = parseInt(params[0] || "");
719
725
  return isNaN(result) ? 0 : result;
720
- }),
721
- float: (params) => {
726
+ } },
727
+ integerArray: {
728
+ parse: (params) => params.map((el) => {
729
+ const result = parseInt(el);
730
+ return isNaN(result) ? 0 : result;
731
+ }),
732
+ serialize: (value) => value.map(String)
733
+ },
734
+ float: { parse: (params) => {
722
735
  const result = parseFloat(params[0] || "");
723
736
  return isNaN(result) ? 0 : result;
737
+ } },
738
+ floatArray: {
739
+ parse: (params) => params.map((el) => {
740
+ const result = parseFloat(el);
741
+ return isNaN(result) ? 0 : result;
742
+ }),
743
+ serialize: (value) => value.map(String)
724
744
  },
725
- floatArray: (params) => params.map((el) => {
726
- const result = parseFloat(el);
727
- return isNaN(result) ? 0 : result;
728
- }),
729
- boolean: (params) => params[0]?.toLowerCase() === "true",
730
- booleanArray: (params) => params.map((el) => el.toLowerCase() === "true"),
731
- zodSchema: (schema) => (params) => {
732
- let parsed;
733
- try {
734
- parsed = JSON.parse(params[0] ?? "{}");
735
- } catch {
736
- throw new Error("Invalid JSON");
737
- }
738
- const result = schema.safeParse(parsed);
739
- if (!result.success) throw new Error("Invalid schema");
740
- return result.data;
745
+ boolean: {
746
+ parse: (params) => params[0]?.toLowerCase() === "true",
747
+ serialize: String
748
+ },
749
+ booleanArray: {
750
+ parse: (params) => params.map((el) => el.toLowerCase() === "true"),
751
+ serialize: (value) => value.map(String)
752
+ },
753
+ date: {
754
+ parse: (params) => new Date(Number(params[0])),
755
+ serialize: (arg) => String(arg.getTime())
756
+ },
757
+ dateArray: {
758
+ parse: (params) => params.map((param) => new Date(Number(param))),
759
+ serialize: (args) => args.map((arg) => String(arg.getTime()))
760
+ },
761
+ zodSchema: {
762
+ parse: (schema) => (params) => {
763
+ let parsed;
764
+ try {
765
+ parsed = JSON.parse(params[0] ?? "{}");
766
+ } catch {
767
+ throw new Error("Invalid JSON");
768
+ }
769
+ const result = schema.safeParse(parsed);
770
+ if (!result.success) throw new Error("Invalid schema");
771
+ return result.data;
772
+ },
773
+ serialize: JSON.stringify
741
774
  }
742
775
  };
743
776
  //#endregion
744
- export { Link, Router, RouterProvider, createRouter, parser, useBeforeUnload, useBlocker, useHistoricalTrail, useLoaderState, useLocation, useNavigate, useParams, useQueryParam, useRouterContext, useSearchParams };
777
+ export { Link, Router, RouterProvider, adapter, createRouter, useBeforeUnload, useBlocker, useHistoricalTrail, useLoaderState, useLocation, useNavigate, useParams, useQueryParam, useRouterContext, useSearchParams };
@@ -55,3 +55,7 @@ export type LoaderState = Record<string, {
55
55
  loaderError: Error | null;
56
56
  beforeLoadError: Error | null;
57
57
  }>;
58
+ export type Adapter<T> = {
59
+ parse: (params: string[]) => T;
60
+ serialize?: (params: T) => string | string[];
61
+ };
@@ -0,0 +1,56 @@
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 adapter: {
11
+ string: {
12
+ parse: (params: string[]) => string;
13
+ };
14
+ stringArray: {
15
+ parse: (params: string[]) => string[];
16
+ serialize: (value: string[]) => string[];
17
+ };
18
+ integer: {
19
+ parse: (params: string[]) => number;
20
+ };
21
+ integerArray: {
22
+ parse: (params: string[]) => number[];
23
+ serialize: (value: number[]) => string[];
24
+ };
25
+ float: {
26
+ parse: (params: string[]) => number;
27
+ };
28
+ floatArray: {
29
+ parse: (params: string[]) => number[];
30
+ serialize: (value: number[]) => string[];
31
+ };
32
+ boolean: {
33
+ parse: (params: string[]) => boolean;
34
+ serialize: StringConstructor;
35
+ };
36
+ booleanArray: {
37
+ parse: (params: string[]) => boolean[];
38
+ serialize: (value: boolean[]) => string[];
39
+ };
40
+ date: {
41
+ parse: (params: string[]) => Date;
42
+ serialize: (arg: Date) => string;
43
+ };
44
+ dateArray: {
45
+ parse: (params: string[]) => Date[];
46
+ serialize: (args: Date[]) => string[];
47
+ };
48
+ zodSchema: {
49
+ parse: <T>(schema: ZodInterface<T>) => (params: string[]) => T;
50
+ serialize: {
51
+ (value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string;
52
+ (value: any, replacer?: (number | string)[] | null, space?: string | number): string;
53
+ };
54
+ };
55
+ };
56
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.3.9",
3
+ "version": "1.4.1",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {
@@ -1,21 +0,0 @@
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 {};