useOptimisticAction
info
useOptimisticAction does not wait for the action to finish execution before returning the optimistic data. It is then synced with the real result from server when the action has finished its execution. If you need to perform normal mutations, use useAction instead.
Let's say you have some todos in your database and want to add a new one. The following example shows how you can use useOptimisticAction to add a new todo item optimistically.
Example
- Define a new action called
addTodo, that takes aTodoobject as input:
"use server";
import { action } from "@/lib/safe-action";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const schema = z.object({
id: z.string().uuid(),
body: z.string().min(1),
completed: z.boolean(),
});
export type Todo = z.infer<typeof schema>;
let todos: Todo[] = [];
export const getTodos = async () => todos;
export const addTodo = action
.metadata({ actionName: "" })
.schema(schema)
.action(async ({ parsedInput }) => {
await new Promise((res) => setTimeout(res, 500));
todos.push(parsedInput);
// This Next.js function revalidates the provided path.
// More info here: https://nextjs.org/docs/app/api-reference/functions/revalidatePath
revalidatePath("/optimistic-hook");
return {
createdTodo: parsedInput,
};
});
- Then, in the parent Server Component, you need to pass the current todos state to the Client Component:
import { getTodos } from "./addtodo-action";
export default function Home() {
return (
<main>
{/* Here we pass current todos to the Client Component.
This is updated on the server every time the action is executed, since we
used `revalidatePath()` inside action's server code. */}
<TodosBox todos={getTodos()} />
</main>
);
}
- Finally, in your Client Component, you can use it like this:
"use client";
import { useOptimisticAction } from "next-safe-action/hooks";
import { addTodo, type Todo } from "@/app/addtodo-action";
type Props = {
todos: Todo[];
};
export default function TodosBox({ todos }: Props) {
const { execute, result, optimisticState } = useOptimisticAction(
addTodo,
{
currentState: { todos }, // gets passed from Server Component
updateFn: (state, newTodo) => {
return {
todos: [...state.todos, newTodo]
};
}
}
);
return (
<div>
<button
onClick={() => {
// Here we execute the action. The input is also passed to `updateFn` as the second argument,
// in this case `newTodo`.
execute({ id: crypto.randomUUID(), body: "New Todo", completed: false });
}}>
Add todo
</button>
{/* Optimistic state gets updated right after the `execute` call (next render), it doesn't wait for the server to respond. */}
<pre>Optimistic state: {optimisticState}</pre>
</div>
);
}
useOptimisticAction arguments
safeActionFn: the safe action that will be called viaexecuteorexecuteAsyncfunctions.utils: object with requiredcurrentStateandupdateFnproperties and optional base utils and callbacks.currentStateis passed from the parent Server Component, andupdateFntells the hook how to update the optimistic state before receiving the server response.
useOptimisticAction return object
execute: an action caller with no return. Input is the same as the safe action you passed to the hook.executeAsync: an action caller that returns a promise with the return value of the safe action. Input is the same as the safe action you passed to the hook.input: the input passed to theexecuteorexecuteAsyncfunction.result: result of the action after its execution.optimisticState: the optimistic state updated right afterexecutecall (on the next render), with the behavior defined inupdateFn.reset: programmatically reset execution state (input,statusandresult).status: string that represents the current action status.isIdle: true if the action status isidle.isTransitioning: true if the transition status from theuseTransitionhook used under the hood istrue.isExecuting: true if the action status isexecuting.isPending: true if the action status isexecutingorisTransitioning.hasSucceeded: true if the action status ishasSucceeded.hasErrored: true if the action status ishasErrored.
For checking the action status, the recommended way is to use the isPending shorthand property. Using isExecuting or checking if status is "executing" could cause race conditions when using navigation functions, such as redirect.
Explore a working example here.