clear-react-router 1.4.0 → 1.4.2

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
@@ -79,7 +79,7 @@ Renders the current route's component. Must be placed inside `<RouterProvider>`.
79
79
  </RouterProvider>
80
80
  ```
81
81
 
82
- > **Note:** When `isAnimated` is enabled, `loaderFallback` is not shown. Instead, a small spinner appears (if `spinner={true}`).
82
+ > **Note:** When `isAnimated` is enabled, `loaderFallback` is not shown. Instead, a small spinner appears (if `spinner={true}`). On the initial page load, however, the route's loaderFallback is rendered if available.
83
83
 
84
84
  ### `Link`
85
85
 
@@ -302,26 +302,23 @@ useBeforeUnload(text ? onSave : undefined);
302
302
 
303
303
  ### `useQueryParam()`
304
304
 
305
- 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.
306
306
 
307
307
  ```
308
- import { useQueryParam, parser } from 'clear-react-router';
308
+ import { useQueryParam, adapter } from 'clear-react-router';
309
309
 
310
310
  const ProductPage = () => {
311
311
  // String parameter
312
- const [brand, setBrand] = useQueryParam('brand', parser.string, 'nike');
312
+ const [brand, setBrand] = useQueryParam('brand', adapter.string, 'nike');
313
313
 
314
314
  // Number parameter
315
- const [page, setPage] = useQueryParam('page', parser.integer, 1);
315
+ const [page, setPage] = useQueryParam('page', adapter.integer, 1);
316
316
 
317
- // Boolean parameter
318
- const [isActive, setIsActive] = useQueryParam('active', parser.boolean, false);
319
-
320
- // Array of strings
321
- const [colors, setColors] = useQueryParam('colors', parser.stringArray);
317
+ // Date parameter
318
+ const [date, setDate] = useQueryParam('date', adapter.date, new Date());
322
319
 
323
320
  // Array of numbers
324
- const [prices, setPrices] = useQueryParam('prices', parser.floatArray);
321
+ const [prices, setPrices] = useQueryParam('prices', adapter.floatArray);
325
322
 
326
323
  return (
327
324
  <div>
@@ -332,12 +329,17 @@ const ProductPage = () => {
332
329
  );
333
330
  }
334
331
  ```
335
- **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]`
336
338
 
337
339
  | Argument | Type | Description |
338
340
  |----------|------|-------------|
339
341
  | `field` | `string` | The query parameter key (e.g., `'page'`, `'brand'`) |
340
- | `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) |
341
343
  | `defaultValue` | `T` (optional) | Default value returned when the parameter is missing or empty |
342
344
 
343
345
  **Returns:**
@@ -345,27 +347,30 @@ const ProductPage = () => {
345
347
  | Element | Type | Description |
346
348
  |---------|------|-------------|
347
349
  | `value` | `T` | The parsed value from the query parameter |
348
- | `setValue` | `(arg: T) => void` | Function to update the query parameter |
349
-
350
- ### Built-in Parsers
351
-
352
- | Parser | Input | Output | Description |
353
- |--------|-------|--------|-------------|
354
- | `parser.string` | `string[]` | `string` | First value or empty string |
355
- | `parser.stringArray` | `string[]` | `string[]` | All values as array |
356
- | `parser.integer` | `string[]` | `number` | First value parsed as integer (default: `0`) |
357
- | `parser.integerArray` | `string[]` | `number[]` | All values parsed as integers |
358
- | `parser.float` | `string[]` | `number` | First value parsed as float (default: `0`) |
359
- | `parser.floatArray` | `string[]` | `number[]` | All values parsed as floats |
360
- | `parser.boolean` | `string[]` | `boolean` | First value parsed as boolean (`'true'` → `true`) |
361
- | `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 |
362
367
 
363
368
  ### Using Zod Schemas
364
369
  `useQueryParam` works seamlessly with Zod for complex validation:
365
370
 
366
371
  ```
367
372
  import { z } from 'zod';
368
- import { useQueryParam, parser } from 'clear-react-router';
373
+ import { useQueryParam, adapter } from 'clear-react-router';
369
374
 
370
375
  const filterSchema = z.object({
371
376
  name: z.string(),
@@ -376,7 +381,7 @@ const filterSchema = z.object({
376
381
  function ProductFilter() {
377
382
  const [filter, setFilter] = useQueryParam(
378
383
  'filter',
379
- parser.zodSchema(filterSchema),
384
+ adapter.zodSchema(filterSchema),
380
385
  { name: '', age: 0 }
381
386
  );
382
387
 
@@ -392,18 +397,21 @@ function ProductFilter() {
392
397
  }
393
398
  ```
394
399
 
395
- ### Custom Parsers
396
- You can write your own parser for any format:
400
+ ### Custom Adapters
401
+ You can write your own adapter for any format:
397
402
 
398
403
  ```
399
- // Custom parser for comma-separated values
400
- const csvParser = (params: string[]): string[] => {
401
- const value = params[0] || '';
402
- return value ? value.split(',').map(v => v.trim()) : [];
403
- };
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
+ }
404
412
 
405
413
  const TagsFilter() {
406
- const [tags, setTags] = useQueryParam('tags', csvParser, []);
414
+ const [tags, setTags] = useQueryParam('tags', csvAdapter, []);
407
415
  // tags: string[]
408
416
  }
409
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
@@ -500,7 +500,6 @@ var renderElement = (Component) => {
500
500
  };
501
501
  //#endregion
502
502
  //#region components/Router.tsx
503
- var PAGE_NOT_FOUND = "error 404. Page not found";
504
503
  var ALL_LOCATIONS = "*";
505
504
  var Router = ({ spinner = true }) => {
506
505
  const { location, isLoading } = useNavigationState();
@@ -510,15 +509,27 @@ var Router = ({ spinner = true }) => {
510
509
  if (!location.pathname) return void 0;
511
510
  return routeList.find((el) => el.path === ALL_LOCATIONS || comparePaths(el, location.pathname));
512
511
  }, [location.pathname, routeList]);
513
- const params = useMemo(() => getParamsObject({
512
+ const pendingRoute = useMemo(() => {
513
+ if (location.pathname) return void 0;
514
+ return routeList.find((el) => comparePaths(el, window.location.pathname));
515
+ }, [location.pathname, routeList]);
516
+ const params = useMemo(() => {
517
+ return getParamsObject({
518
+ routeItem: routeItem || pendingRoute,
519
+ pathname: location.pathname || window.location.pathname
520
+ });
521
+ }, [
522
+ location.pathname,
514
523
  routeItem,
515
- pathname: window.location.pathname
516
- }), [routeItem]);
524
+ pendingRoute
525
+ ]);
517
526
  const shouldErrorElementShown = useMemo(() => Boolean(loaderState[location.pathname]?.loaderError || loaderState[location.pathname]?.beforeLoadError), [loaderState, location.pathname]);
518
- if (spinner && !routeItem && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
519
- if (!routeItem && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {});
520
- if (!routeItem) return null;
521
- if (!isAnimated && routeItem?.loader && !shouldErrorElementShown && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewProvider, {
527
+ if (!routeItem && !pendingRoute) return null;
528
+ if (!routeItem && pendingRoute) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewProvider, {
529
+ params,
530
+ children: renderElement(pendingRoute?.loaderFallback)
531
+ });
532
+ if (!isAnimated && !shouldErrorElementShown && isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewProvider, {
522
533
  params,
523
534
  children: renderElement(routeItem?.loaderFallback)
524
535
  });
@@ -530,7 +541,7 @@ var Router = ({ spinner = true }) => {
530
541
  style: { viewTransitionName: "page" },
531
542
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ViewProvider, {
532
543
  params,
533
- children: [renderElement(routeItem?.element) || PAGE_NOT_FOUND, spinner && isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
544
+ children: [renderElement(routeItem?.element) || null, spinner && isAnimated && isLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {})]
534
545
  })
535
546
  });
536
547
  };
@@ -675,21 +686,28 @@ var useSearchParams = () => {
675
686
  };
676
687
  //#endregion
677
688
  //#region hooks/useQueryParam.ts
678
- function useQueryParam(field, parser, defaultValue) {
689
+ function useQueryParam(field, adapter, defaultValue) {
679
690
  const { searchParams, setSearchParams } = useSearchParams();
680
691
  return [useMemo(() => {
681
- const result = parser(searchParams.getAll(field));
682
- if (result !== void 0 && result !== null && result !== "") return result;
692
+ const params = searchParams.getAll(field);
693
+ const result = adapter.parse(params);
694
+ const isValid = !(result instanceof Date) || result instanceof Date && !isNaN(result.getTime());
695
+ if (result !== void 0 && result !== null && result !== "" && isValid) return result;
683
696
  if (defaultValue !== void 0) return defaultValue;
684
697
  return result;
685
698
  }, [
686
699
  field,
687
- parser,
700
+ adapter,
688
701
  searchParams,
689
702
  defaultValue
690
703
  ]), useCallback((value) => {
691
- setSearchParams(field, (typeof value === "object" ? JSON.stringify : String)(value));
692
- }, [setSearchParams, field])];
704
+ if (!value) return setSearchParams(field, []);
705
+ setSearchParams(field, (adapter.serialize || String)(value));
706
+ }, [
707
+ field,
708
+ adapter.serialize,
709
+ setSearchParams
710
+ ])];
693
711
  }
694
712
  //#endregion
695
713
  //#region hooks/useHistoricalTrail.ts
@@ -706,39 +724,65 @@ var useHistoricalTrail = () => {
706
724
  return trail;
707
725
  };
708
726
  //#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;
727
+ //#region utils/adapter.ts
728
+ var adapter = {
729
+ string: { parse: (params) => params[0] || "" },
730
+ stringArray: {
731
+ parse: (params) => params,
732
+ serialize: (value) => value
716
733
  },
717
- integerArray: (params) => params.map((el) => {
718
- const result = parseInt(el, 10);
734
+ integer: { parse: (params) => {
735
+ const result = parseInt(params[0] || "");
719
736
  return isNaN(result) ? 0 : result;
720
- }),
721
- float: (params) => {
737
+ } },
738
+ integerArray: {
739
+ parse: (params) => params.map((el) => {
740
+ const result = parseInt(el);
741
+ return isNaN(result) ? 0 : result;
742
+ }),
743
+ serialize: (value) => value.map(String)
744
+ },
745
+ float: { parse: (params) => {
722
746
  const result = parseFloat(params[0] || "");
723
747
  return isNaN(result) ? 0 : result;
748
+ } },
749
+ floatArray: {
750
+ parse: (params) => params.map((el) => {
751
+ const result = parseFloat(el);
752
+ return isNaN(result) ? 0 : result;
753
+ }),
754
+ serialize: (value) => value.map(String)
724
755
  },
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;
756
+ boolean: {
757
+ parse: (params) => params[0]?.toLowerCase() === "true",
758
+ serialize: String
759
+ },
760
+ booleanArray: {
761
+ parse: (params) => params.map((el) => el.toLowerCase() === "true"),
762
+ serialize: (value) => value.map(String)
763
+ },
764
+ date: {
765
+ parse: (params) => new Date(Number(params[0])),
766
+ serialize: (arg) => String(arg.getTime())
767
+ },
768
+ dateArray: {
769
+ parse: (params) => params.map((param) => new Date(Number(param))),
770
+ serialize: (args) => args.map((arg) => String(arg.getTime()))
771
+ },
772
+ zodSchema: {
773
+ parse: (schema) => (params) => {
774
+ let parsed;
775
+ try {
776
+ parsed = JSON.parse(params[0] ?? "{}");
777
+ } catch {
778
+ throw new Error("Invalid JSON");
779
+ }
780
+ const result = schema.safeParse(parsed);
781
+ if (!result.success) throw new Error("Invalid schema");
782
+ return result.data;
783
+ },
784
+ serialize: JSON.stringify
741
785
  }
742
786
  };
743
787
  //#endregion
744
- export { Link, Router, RouterProvider, createRouter, parser, useBeforeUnload, useBlocker, useHistoricalTrail, useLoaderState, useLocation, useNavigate, useParams, useQueryParam, useRouterContext, useSearchParams };
788
+ 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.4.0",
3
+ "version": "1.4.2",
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 {};