Skip to content

Commit e0f5c20

Browse files
authoredMay 27, 2024··
feat(hooks): return executeAsync from useAction and useOptimisticAction hooks (#147)
This PR adds the `executeAsync` function to the return object of `useAction` and `useOptimisticAction` hooks. It's currently not possible to add it to the `useStateAction` hook. As the name suggests, `executeAsync` returns a Promise with the same result of the `safeActionFn` you pass to the hook, and it allows to await the result of the action. re #137, #72, #94
1 parent 4e9d3b2 commit e0f5c20

File tree

5 files changed

+75
-4
lines changed

5 files changed

+75
-4
lines changed
 

‎apps/playground/src/app/(examples)/hook/page.tsx

+6-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { deleteUser } from "./deleteuser-action";
1010
export default function Hook() {
1111
// Safe action (`deleteUser`) and optional callbacks passed to `useAction` hook.
1212
const {
13-
execute,
13+
executeAsync,
1414
result,
1515
status,
1616
reset,
@@ -40,15 +40,17 @@ export default function Hook() {
4040
<StyledHeading>Action using hook</StyledHeading>
4141
<form
4242
className="flex flex-col mt-8 space-y-4"
43-
onSubmit={(e) => {
43+
onSubmit={async (e) => {
4444
e.preventDefault();
4545
const formData = new FormData(e.currentTarget);
4646
const input = Object.fromEntries(formData) as {
4747
userId: string;
4848
};
4949

50-
// Action call.
51-
execute(input);
50+
// Action call. Here we use `executeAsync` that lets us await the result. You can also use the `execute` function,
51+
// which is synchronous.
52+
const r = await executeAsync(input);
53+
console.log("r", r);
5254
}}>
5355
<StyledInput
5456
type="text"

‎packages/next-safe-action/src/hooks.ts

+63
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,36 @@ export const useAction = <
168168
[safeActionFn]
169169
);
170170

171+
const executeAsync = React.useCallback(
172+
(input: S extends Schema ? InferIn<S> : void) => {
173+
setIsIdle(false);
174+
setClientInput(input);
175+
setIsExecuting(true);
176+
177+
return new Promise<Awaited<ReturnType<typeof safeActionFn>>>((resolve, reject) => {
178+
startTransition(() => {
179+
safeActionFn(input as S extends Schema ? InferIn<S> : undefined)
180+
.then((res) => {
181+
setResult(res ?? EMPTY_HOOK_RESULT);
182+
resolve(res);
183+
})
184+
.catch((e) => {
185+
if (isRedirectError(e) || isNotFoundError(e)) {
186+
throw e;
187+
}
188+
189+
setResult({ fetchError: isError(e) ? e.message : "Something went wrong" });
190+
reject(e);
191+
})
192+
.finally(() => {
193+
setIsExecuting(false);
194+
});
195+
});
196+
});
197+
},
198+
[safeActionFn]
199+
);
200+
171201
const reset = () => {
172202
setIsIdle(true);
173203
setClientInput(undefined);
@@ -183,6 +213,7 @@ export const useAction = <
183213

184214
return {
185215
execute,
216+
executeAsync,
186217
input: clientInput,
187218
result,
188219
reset,
@@ -250,6 +281,37 @@ export const useOptimisticAction = <
250281
[safeActionFn, setOptimisticValue]
251282
);
252283

284+
const executeAsync = React.useCallback(
285+
(input: S extends Schema ? InferIn<S> : void) => {
286+
setIsIdle(false);
287+
setClientInput(input);
288+
setIsExecuting(true);
289+
290+
return new Promise<Awaited<ReturnType<typeof safeActionFn>>>((resolve, reject) => {
291+
startTransition(() => {
292+
setOptimisticValue(input as S extends Schema ? InferIn<S> : undefined);
293+
safeActionFn(input as S extends Schema ? InferIn<S> : undefined)
294+
.then((res) => {
295+
setResult(res ?? EMPTY_HOOK_RESULT);
296+
resolve(res);
297+
})
298+
.catch((e) => {
299+
if (isRedirectError(e) || isNotFoundError(e)) {
300+
throw e;
301+
}
302+
303+
setResult({ fetchError: isError(e) ? e.message : "Something went wrong" });
304+
reject(e);
305+
})
306+
.finally(() => {
307+
setIsExecuting(false);
308+
});
309+
});
310+
});
311+
},
312+
[safeActionFn, setOptimisticValue]
313+
);
314+
253315
const reset = () => {
254316
setIsIdle(true);
255317
setClientInput(undefined);
@@ -270,6 +332,7 @@ export const useOptimisticAction = <
270332

271333
return {
272334
execute,
335+
executeAsync,
273336
input: clientInput,
274337
result,
275338
optimisticState,

‎website/docs/execution/hooks/useaction.md

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ As you can see, here we display a greet message after the action is performed, i
7575
| Name | Type | Purpose |
7676
| --------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------- |
7777
| `execute` | `(input: InferIn<S>) => void` | An action caller with no return. The input is the same as the safe action you passed to the hook. |
78+
| `executeAsync` | `(input: InferIn<S>) => Promise<Awaited<ReturnType<typeof safeActionFn>>>` | An action caller that returns a promise with the return value of the safe action. The input is the same as the safe action you passed to the hook. |
7879
| `input` | `InferIn<S> \| undefined` | The input passed to the `execute` function. |
7980
| `result` | [`HookResult`](/docs/types#hookresult) | When the action gets called via `execute`, this is the result object. |
8081
| `reset` | `() => void` | Programmatically reset `input` and `result` object with this function. |

‎website/docs/execution/hooks/useoptimisticaction.md

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export default function TodosBox({ todos }: Props) {
135135
| Name | Type | Purpose |
136136
| ---------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
137137
| `execute` | `(input: InferIn<S>) => void` | An action caller with no return. The input is the same as the safe action you passed to the hook. |
138+
| `executeAsync` | `(input: InferIn<S>) => Promise<Awaited<ReturnType<typeof safeActionFn>>>` | An action caller that returns a promise with the return value of the safe action. The input is the same as the safe action you passed to the hook. |
138139
| `input` | `InferIn<S> \| undefined` | The input passed to the `execute` function. |
139140
| `result` | [`HookResult`](/docs/types#hookresult) | When the action gets called via `execute`, this is the result object. |
140141
| `optimisticState` | `State` | This contains the state that gets updated immediately after `execute` is called, with the behavior you defined in the `updateFn` function. The initial state is what you provided to the hook via `currentState` argument. If an error occurs during action execution, the `optimisticState` reverts to the state that it had pre-last-last action execution. |

‎website/docs/migrations/v6-to-v7.md

+4
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ Sometimes it's useful to access the input passed to an action when using hooks.
157157

158158
Starting from version 7, `isIdle`, `isExecuting`, `hasSucceeded` and `hasErrored` are returned from hooks, in addition to the `status` property. This is the same behavior of next-safe-action pre-v4 and very similar to the [TanStack Query](https://tanstack.com/query/latest) API.
159159

160+
### [Return `executeAsync` from `useAction` and `useOptimisticAction` hooks](https://github.com/TheEdoRan/next-safe-action/issues/146)
161+
162+
Sometimes it's useful to await the result of an action execution. Starting from version 7, `executeAsync` is returned from `useAction` and `useOptimisticAction` hooks. It's essentially the same as the original safe action function, with the added benefits from the hooks. It's currently not possible to add this function to the `useStateAction` hook, due to internal React limitations.
163+
160164
## Refactors
161165

162166
### `serverCodeFn` signature

0 commit comments

Comments
 (0)
Please sign in to comment.