Skip to content

Commit

Permalink
feat(hooks): implement executeOnMount functionality (#207)
Browse files Browse the repository at this point in the history
This PR introduces the `executeOnMount` functionality for hooks, to execute the action when the component mounts, immediately or after a specified number of milliseconds, via `delayMs` prop.

re #191
  • Loading branch information
TheEdoRan authored Jul 21, 2024
1 parent 4089f87 commit 84f94fb
Show file tree
Hide file tree
Showing 12 changed files with 224 additions and 223 deletions.
8 changes: 4 additions & 4 deletions apps/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"lucide-react": "^0.378.0",
"next": "15.0.0-canary.25",
"next": "15.0.0-canary.75",
"next-safe-action": "workspace:*",
"react": "19.0.0-rc-6230622a1a-20240610",
"react-dom": "19.0.0-rc-6230622a1a-20240610",
"react": "19.0.0-rc-512b09b2-20240718",
"react-dom": "19.0.0-rc-512b09b2-20240718",
"react-hook-form": "^7.51.4",
"zod": "^3.23.6",
"zod-form-data": "^2.0.2"
Expand All @@ -26,7 +26,7 @@
"@types/react-dom": "18.3.0",
"autoprefixer": "10.4.19",
"eslint": "^8.57.0",
"eslint-config-next": "15.0.0-canary.25",
"eslint-config-next": "15.0.0-canary.75",
"postcss": "8.4.38",
"tailwindcss": "3.4.3",
"typescript": "^5.5.3"
Expand Down
23 changes: 22 additions & 1 deletion packages/next-safe-action/src/hooks-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from "react";
import {} from "react/experimental";
import type {} from "zod";
import type { InferIn, Schema } from "./adapters/types";
import type { HookActionStatus, HookCallbacks, HookResult } from "./hooks.types";
import type { HookActionStatus, HookBaseUtils, HookCallbacks, HookResult } from "./hooks.types";

export const getActionStatus = <
ServerError,
Expand Down Expand Up @@ -45,6 +45,27 @@ export const getActionShorthandStatusObject = (status: HookActionStatus) => {
};
};

export const useExecuteOnMount = <S extends Schema | undefined>(
args: HookBaseUtils<S> & {
executeFn: (input: S extends Schema ? InferIn<S> : void) => void;
}
) => {
const mounted = React.useRef(false);

React.useEffect(() => {
const t = setTimeout(() => {
if (args.executeOnMount && !mounted.current) {
args.executeFn(args.executeOnMount.input);
mounted.current = true;
}
}, args.executeOnMount?.delayMs ?? 0);

return () => {
clearTimeout(t);
};
}, [args]);
};

export const useActionCallbacks = <
ServerError,
S extends Schema | undefined,
Expand Down
25 changes: 18 additions & 7 deletions packages/next-safe-action/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import * as ReactDOM from "react-dom";
import {} from "react/experimental";
import type {} from "zod";
import type { InferIn, Schema } from "./adapters/types";
import { getActionShorthandStatusObject, getActionStatus, useActionCallbacks } from "./hooks-utils";
import type { HookCallbacks, HookResult, HookSafeActionFn } from "./hooks.types";
import { getActionShorthandStatusObject, getActionStatus, useActionCallbacks, useExecuteOnMount } from "./hooks-utils";
import type { HookBaseUtils, HookCallbacks, HookResult, HookSafeActionFn } from "./hooks.types";
import { isError } from "./utils";

// HOOKS

/**
* Use the action from a Client Component via hook.
* @param safeActionFn The action function
* @param utils Optional callbacks
* @param utils Optional base utils and callbacks
*
* {@link https://next-safe-action.dev/docs/execution/hooks/useaction See docs for more information}
*/
Expand All @@ -29,7 +29,7 @@ export const useAction = <
Data,
>(
safeActionFn: HookSafeActionFn<ServerError, S, BAS, CVE, CBAVE, Data>,
utils?: HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>
utils?: HookBaseUtils<S> & HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>
) => {
const [, startTransition] = React.useTransition();
const [result, setResult] = React.useState<HookResult<ServerError, S, BAS, CVE, CBAVE, Data>>({});
Expand Down Expand Up @@ -105,11 +105,16 @@ export const useAction = <
setResult({});
}, []);

useExecuteOnMount({
executeOnMount: utils?.executeOnMount,
executeFn: execute,
});

useActionCallbacks({
result: result ?? {},
input: clientInput as S extends Schema ? InferIn<S> : undefined,
status,
cb: utils,
cb: { onSuccess: utils?.onSuccess, onError: utils?.onError, onSettled: utils?.onSettled },
});

return {
Expand All @@ -126,7 +131,7 @@ export const useAction = <
/**
* Use the action from a Client Component via hook, with optimistic data update.
* @param safeActionFn The action function
* @param utils Required `currentData` and `updateFn` and optional callbacks
* @param utils Required `currentData` and `updateFn` and optional base utils and callbacks
*
* {@link https://next-safe-action.dev/docs/execution/hooks/useoptimisticaction See docs for more information}
*/
Expand All @@ -143,7 +148,8 @@ export const useOptimisticAction = <
utils: {
currentState: State;
updateFn: (state: State, input: S extends Schema ? InferIn<S> : undefined) => State;
} & HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>
} & HookBaseUtils<S> &
HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>
) => {
const [, startTransition] = React.useTransition();
const [result, setResult] = React.useState<HookResult<ServerError, S, BAS, CVE, CBAVE, Data>>({});
Expand Down Expand Up @@ -225,6 +231,11 @@ export const useOptimisticAction = <
setResult({});
}, []);

useExecuteOnMount({
executeOnMount: utils?.executeOnMount,
executeFn: execute,
});

useActionCallbacks({
result: result ?? {},
input: clientInput as S extends Schema ? InferIn<S> : undefined,
Expand Down
10 changes: 10 additions & 0 deletions packages/next-safe-action/src/hooks.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ import type { InferIn, Schema } from "./adapters/types";
import type { SafeActionResult } from "./index.types";
import type { MaybePromise, Prettify } from "./utils.types";

/**
* Type of base utils object passed to `useAction`, `useOptimisticAction` and `useStateAction` hooks.
*/
export type HookBaseUtils<S extends Schema | undefined> = {
executeOnMount?: {
input: S extends Schema ? InferIn<S> : undefined;
delayMs?: number;
};
};

/**
* Type of `result` object returned by `useAction`, `useOptimisticAction` and `useStateAction` hooks.
* If a server-client communication error occurs, `fetchError` will be set to the error message.
Expand Down
19 changes: 14 additions & 5 deletions packages/next-safe-action/src/stateful-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import * as ReactDOM from "react-dom";
import {} from "react/experimental";
import type {} from "zod";
import type { InferIn, Schema } from "./adapters/types";
import { getActionShorthandStatusObject, getActionStatus, useActionCallbacks } from "./hooks-utils";
import type { HookCallbacks, HookSafeStateActionFn } from "./hooks.types";
import { getActionShorthandStatusObject, getActionStatus, useActionCallbacks, useExecuteOnMount } from "./hooks-utils";
import type { HookBaseUtils, HookCallbacks, HookSafeStateActionFn } from "./hooks.types";
/**
* Use the stateful action from a Client Component via hook. Used for actions defined with [`stateAction`](https://next-safe-action.dev/docs/safe-action-client/instance-methods#action--stateaction).
* @param safeActionFn The action function
* @param utils Optional `initResult`, `permalink` and callbacks
* @param utils Optional `initResult`, `permalink`, base utils and callbacks
*
* {@link https://next-safe-action.dev/docs/execution/hooks/usestateaction See docs for more information}
*/
Expand All @@ -26,14 +26,16 @@ export const useStateAction = <
utils?: {
initResult?: Awaited<ReturnType<typeof safeActionFn>>;
permalink?: string;
} & HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>
} & HookBaseUtils<S> &
HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>
) => {
const [result, dispatcher, isExecuting] = React.useActionState(
safeActionFn,
utils?.initResult ?? {},
utils?.permalink
);
const [isIdle, setIsIdle] = React.useState(true);
const [, startTransition] = React.useTransition();
const [clientInput, setClientInput] = React.useState<S extends Schema ? InferIn<S> : void>();
const status = getActionStatus<ServerError, S, BAS, CVE, CBAVE, Data>({
isExecuting,
Expand All @@ -43,7 +45,9 @@ export const useStateAction = <

const execute = React.useCallback(
(input: S extends Schema ? InferIn<S> : void) => {
dispatcher(input as S extends Schema ? InferIn<S> : undefined);
startTransition(() => {
dispatcher(input as S extends Schema ? InferIn<S> : undefined);
});

ReactDOM.flushSync(() => {
setIsIdle(false);
Expand All @@ -53,6 +57,11 @@ export const useStateAction = <
[dispatcher]
);

useExecuteOnMount({
executeOnMount: utils?.executeOnMount,
executeFn: execute,
});

useActionCallbacks({
result: result ?? {},
input: clientInput as S extends Schema ? InferIn<S> : undefined,
Expand Down
Loading

0 comments on commit 84f94fb

Please sign in to comment.