clear-react-router 1.6.9 → 1.7.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
@@ -264,15 +264,15 @@ Actions can be executed declaratively with `<Form />` or imperatively with `useA
264
264
 
265
265
  ## Form
266
266
 
267
- `Form` automatically creates a `FormData` object, executes the specified route action, invalidates the current route, and optionally resets the form.
267
+ `Form` automatically creates a `FormData` object on submit event, executes the specified route action, invalidates the current route, and optionally resets the form, if fields are uncontrolled.
268
268
 
269
269
  `isSubmitting` value available inside the `Form` component from the `useFormContext` hook
270
270
 
271
271
  ```tsx
272
- import { Form, useFormContext } from '../clear-router';
272
+ import { Form, useFormContext } from 'clear-react-router';
273
273
 
274
274
  const SubmitButton = () => {
275
- const { isSubmitting } = useFormContext()
275
+ const {isSubmitting} = useFormContext()
276
276
  return <button disabled={isSubmitting} type='submit'>Save</button>
277
277
  }
278
278
 
@@ -305,22 +305,23 @@ After a successful action:
305
305
 
306
306
  `useAction` provides direct access to a route action without rendering a `<Form />`.
307
307
 
308
+ ### Arguments
309
+
310
+ | Argument | Type | Description |
311
+ |----------|------|-------------|
312
+ | `action` | `string` | Name of the route action to execute. Must match a key returned from the route's `actions` configuration. |
313
+ | `options` | `{ onSuccess?, onError? }` | Optional callbacks invoked after the action succeeds or fails. |
314
+
308
315
  ```tsx
309
316
  const save = useAction('save');
310
317
 
311
318
  const handleClick = async () => {
312
319
  const data = new FormData();
313
-
314
320
  data.append('title', 'Hello');
315
-
316
321
  await save(data);
317
322
  };
318
- ```
319
323
 
320
- ```tsx
321
- <button onClick={handleClick}>
322
- Save
323
- </button>
324
+ <button onClick={handleClick}>Save</button>
324
325
  ```
325
326
 
326
327
  `useAction` automatically invalidates the current route after a successful action, causing both `beforeLoad` and `loader` to run again in the background.
@@ -508,29 +509,6 @@ useEffect(() => {
508
509
  }, [state, process, reset]);
509
510
  ```
510
511
 
511
- ### `useBeforeUnload(callback?)`
512
-
513
- Executes a callback when the page is about to be closed or reloaded. Perfect for auto-saving data at the last moment.
514
-
515
- **Parameters:**
516
-
517
- | Parameter | Type | Description |
518
- |-----------|------|-------------|
519
- | `callback` | `() => void \| undefined` | Function to execute before page unload (e.g., auto-save) |
520
-
521
- **Note:** This hook does not show a browser confirmation dialog. It silently executes the callback, allowing you to save user data in the background before the page closes.
522
-
523
- ```tsx
524
- const [text, setText] = useState('');
525
- const onSave = useCallback(() => {
526
- localStorage.setItem('draft', text);
527
- }, [text]);
528
-
529
- // Auto-save when user tries to close/reload the page
530
- useBeforeUnload(text ? onSave : undefined);
531
- ```
532
- > **Note:** Pass `undefined` to disable the handler (e.g., if there is no changes).
533
-
534
512
  ### `useQueryParam()`
535
513
 
536
514
  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.
@@ -701,18 +679,6 @@ function ProductFilter() {
701
679
 
702
680
  > **Note:** `getSearchParams` returns `string` for single values, `string[]` for multiple values, and `''` if the key is not found.
703
681
 
704
- ### `useHistoricalTrail()`
705
-
706
- 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.
707
-
708
- **Returns:** Array of pathnames in chronological visit order (e.g., `['/dashboard', '/users', '/settings']`)
709
-
710
- **Key features:**
711
- - **Chronological order** — Paths are stored in the order the user visited them
712
- - **Unique entries** — Revisiting a page trims the trail to that point
713
- - **Respects navigation blocking** — Only successful navigations are added
714
- - **Redirect-safe** — Redirected pages are not added to the trail
715
-
716
682
  ## Lazy Loading
717
683
 
718
684
  Clear Router supports code-splitting out of the box. Simply pass a function that returns a dynamic import:
@@ -5,5 +5,5 @@ type FormProps = {
5
5
  onError?(arg: unknown): void;
6
6
  autoReset?: boolean;
7
7
  };
8
- export declare const Form: ({ children, action: actionKey, onSuccess, onError, autoReset, }: PropsWithChildren<FormProps>) => import("react/jsx-runtime").JSX.Element;
8
+ export declare const Form: ({ children, action, onSuccess, onError, autoReset }: PropsWithChildren<FormProps>) => import("react/jsx-runtime").JSX.Element;
9
9
  export {};
@@ -1 +1,6 @@
1
- export declare const useAction: (actionKey: string, onError?: (args: unknown) => void) => (formData: FormData) => Promise<void>;
1
+ type Options = Partial<{
2
+ onSuccess: (args: unknown) => void;
3
+ onError: (args: unknown) => void;
4
+ }> | undefined;
5
+ export declare const useAction: (action: string, options?: Options) => (formData: FormData) => Promise<void>;
6
+ export {};
@@ -0,0 +1,4 @@
1
+ export declare const useGetAction: (actionKey: string) => {
2
+ currentAction: (arg: FormData) => Promise<unknown> | Promise<void> | void | unknown;
3
+ invalidate: (path?: string) => Promise<void>;
4
+ };
package/dist/index.d.ts CHANGED
@@ -9,11 +9,9 @@ export { useLoaderState } from './hooks/useLoaderState';
9
9
  export { useInvalidate } from './hooks/useInvalidate';
10
10
  export { useBlocker } from './hooks/useBlocker';
11
11
  export { useAction } from './hooks/useAction';
12
- export { useBeforeUnload } from './hooks/useBeforeUnload';
13
12
  export { useRouterContext } from './hooks/useRouterContext';
14
13
  export { useQueryParam } from './hooks/useQueryParam';
15
14
  export { useSearchParams } from './hooks/useSearchParams';
16
- export { useHistoricalTrail } from './hooks/useHistoricalTrail';
17
15
  export { useFormContext } from './hooks/useFormContext';
18
16
  export { adapter } from './utils/adapter';
19
17
  export { createRouter } from './utils/utils';
package/dist/index.js CHANGED
@@ -704,6 +704,15 @@ var Link = ({ children, to, prefetch: prefetchLink, hoverPrefetchDelay }) => {
704
704
  });
705
705
  };
706
706
  //#endregion
707
+ //#region context/FormContext.ts
708
+ var FormContext = createContext({ isSubmitting: false });
709
+ //#endregion
710
+ //#region provider/FormProvider.tsx
711
+ var FormProvider = ({ children, isSubmitting }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FormContext.Provider, {
712
+ value: { isSubmitting },
713
+ children
714
+ });
715
+ //#endregion
707
716
  //#region hooks/useInvalidate.ts
708
717
  var useInvalidate = () => {
709
718
  const { invalidate } = useRouterActions();
@@ -714,44 +723,44 @@ var useInvalidate = () => {
714
723
  var useParams = () => {
715
724
  const [routeItemData] = useRouteItemData();
716
725
  const { routeItem, location: { pathname } } = routeItemData;
717
- if (!routeItem) return void 0;
718
- return getParamsObject({
726
+ return useMemo(() => routeItem ? getParamsObject({
719
727
  params: routeItem?.params,
720
728
  pathname
721
- });
729
+ }) : void 0, [pathname, routeItem]);
722
730
  };
723
731
  //#endregion
724
- //#region context/FormContext.ts
725
- var FormContext = createContext({ isSubmitting: false });
726
- //#endregion
727
- //#region provider/FormProvider.tsx
728
- var FormProvider = ({ children, isSubmitting }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FormContext.Provider, {
729
- value: { isSubmitting },
730
- children
731
- });
732
- //#endregion
733
- //#region components/Form.tsx
734
- var Form = ({ children, action: actionKey, onSuccess, onError, autoReset = true }) => {
732
+ //#region hooks/useGetAction.ts
733
+ var useGetAction = (actionKey) => {
735
734
  const invalidate = useInvalidate();
736
735
  const [routeItemData] = useRouteItemData();
737
736
  const [context, setContext] = useContextState();
738
737
  const params = useParams();
739
738
  const { routeItem } = routeItemData;
739
+ const latestContext = useLatest(context);
740
+ if (!routeItem) throw new Error("Route not found");
741
+ if (!routeItem.actions) throw new Error("Route action creator not found");
742
+ const action = routeItem.actions({
743
+ context: latestContext.current,
744
+ setContext,
745
+ params,
746
+ invalidate
747
+ })[actionKey];
748
+ if (!action) throw new Error(`Action "${actionKey}" not found`);
749
+ return {
750
+ currentAction: action,
751
+ invalidate
752
+ };
753
+ };
754
+ //#endregion
755
+ //#region components/Form.tsx
756
+ var Form = ({ children, action, onSuccess, onError, autoReset = true }) => {
757
+ const { currentAction, invalidate } = useGetAction(action);
740
758
  const [isSubmitting, setIsSubmitting] = useState(false);
741
759
  const onSubmit = async (evt) => {
742
760
  evt.preventDefault();
743
- if (!routeItem) throw new Error("Route not found");
744
- if (!routeItem.actions) throw new Error("Route action creator not found");
745
- const action = routeItem.actions({
746
- context,
747
- setContext,
748
- params,
749
- invalidate
750
- })[actionKey];
751
- if (!action) throw new Error(`Action "${actionKey}" not found`);
752
761
  try {
753
762
  setIsSubmitting(true);
754
- const result = await action(new FormData(evt.target));
763
+ const result = await currentAction(new FormData(evt.target));
755
764
  await invalidate();
756
765
  if (autoReset) evt.target.reset();
757
766
  onSuccess?.(result);
@@ -821,53 +830,26 @@ var useBlocker = (blockerFn) => {
821
830
  };
822
831
  //#endregion
823
832
  //#region hooks/useAction.ts
824
- var useAction = (actionKey, onError) => {
825
- const invalidate = useInvalidate();
826
- const [routeItemData] = useRouteItemData();
827
- const [context, setContext] = useContextState();
828
- const params = useParams();
829
- const { routeItem } = routeItemData;
830
- const latestContext = useLatest(context);
833
+ var useAction = (action, options = {}) => {
834
+ const { currentAction, invalidate } = useGetAction(action);
835
+ const latestOnSuccess = useLatest(options?.onSuccess);
836
+ const latestOnError = useLatest(options?.onError);
831
837
  return useCallback(async (formData) => {
832
- if (!routeItem) throw new Error("Route not found");
833
- if (!routeItem.actions) throw new Error("Route action creator not found");
834
- const action = routeItem.actions({
835
- context: latestContext.current,
836
- setContext,
837
- params,
838
- invalidate
839
- })[actionKey];
840
- if (!action) throw new Error(`Action "${actionKey}" not found`);
841
838
  try {
842
- await action(formData);
839
+ const result = await currentAction(formData);
843
840
  await invalidate();
841
+ latestOnSuccess.current?.(result);
844
842
  } catch (error) {
845
- onError?.(error);
843
+ latestOnError.current?.(error);
846
844
  }
847
845
  }, [
848
- actionKey,
849
- latestContext,
846
+ currentAction,
850
847
  invalidate,
851
- onError,
852
- params,
853
- routeItem,
854
- setContext
848
+ latestOnError,
849
+ latestOnSuccess
855
850
  ]);
856
851
  };
857
852
  //#endregion
858
- //#region hooks/useBeforeUnload.ts
859
- var useBeforeUnload = (callback) => {
860
- useEffect(() => {
861
- const handler = (event) => {
862
- if (!callback) return;
863
- event.preventDefault();
864
- callback();
865
- };
866
- window.addEventListener("beforeunload", handler);
867
- return () => window.removeEventListener("beforeunload", handler);
868
- }, [callback]);
869
- };
870
- //#endregion
871
853
  //#region hooks/useRouterContext.ts
872
854
  var useRouterContext = () => {
873
855
  const [context, setContext] = useContextState();
@@ -962,20 +944,6 @@ function useQueryParam(field, adapter, defaultValue) {
962
944
  ])];
963
945
  }
964
946
  //#endregion
965
- //#region hooks/useHistoricalTrail.ts
966
- var useHistoricalTrail = () => {
967
- const { pathname } = useLocation();
968
- const [trail, setTrail] = useState([]);
969
- useEffect(() => {
970
- if (!pathname) return;
971
- setTrail((prevState) => {
972
- const index = prevState.indexOf(pathname);
973
- return index === -1 ? [...prevState, pathname] : prevState.slice(0, index + 1);
974
- });
975
- }, [pathname]);
976
- return trail;
977
- };
978
- //#endregion
979
947
  //#region hooks/useFormContext.ts
980
948
  var useFormContext = () => useContext(FormContext);
981
949
  //#endregion
@@ -1041,4 +1009,4 @@ var adapter = {
1041
1009
  })
1042
1010
  };
1043
1011
  //#endregion
1044
- export { Form, Link, Router, RouterProvider, adapter, createRouter, useAction, useBeforeUnload, useBlocker, useFormContext, useHistoricalTrail, useInvalidate, useLoaderState, useLocation, useNavigate, useParams, useQueryParam, useRouterContext, useSearchParams };
1012
+ export { Form, Link, Router, RouterProvider, adapter, createRouter, useAction, useBlocker, useFormContext, useInvalidate, useLoaderState, useLocation, useNavigate, useParams, useQueryParam, useRouterContext, useSearchParams };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clear-react-router",
3
- "version": "1.6.9",
3
+ "version": "1.7.1",
4
4
  "description": "A lightweight, type-safe routing library for React applications",
5
5
  "author": "Andrew Bubnov",
6
6
  "scripts": {
@@ -1 +0,0 @@
1
- export declare const useBeforeUnload: (callback?: () => void) => void;
@@ -1 +0,0 @@
1
- export declare const useHistoricalTrail: () => string[];