Skip to main content

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 want to update the number of likes of a post in your application, mutating directly the database.

Example

  1. Define a new action called addLikes, that takes an amount as input and returns the updated number of likes:
"use server";

const schema = z.object({
incrementBy: z.number().positive(),
});

// Fake database.
let likes = 42;
export const getLikes = () => likes;

export const addLikes = actionClient
.schema(schema)
.action(async ({ parsedInput: { incrementBy } }) => {
await new Promise((resolve) => setTimeout(resolve, 1000));

// Mutate data in fake db. This would be a database call in the real world.
likes += incrementBy;

// We use this function to revalidate server state.
// More information about it here:
// https://nextjs.org/docs/app/api-reference/functions/revalidatePath
revalidatePath("/");

return { likesCount: likes };
});
  1. Then, in your Server Component, you need to pass the current number of likes to the Client Component:
export default function Home() {
return (
<main>
{/* Here we pass current number of likes 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. */}
<AddLikes likesCount={getLikes()} />
</main>
);
}
  1. Finally, in your Client Component, you can use it like this:
"use client";

import { useOptimisticAction } from "next-safe-action/hooks";
import { addLikes } from "@/app/add-likes-action";

type Props = {
likesCount: number;
};

export default function AddLikes({ likesCount }: Props) {
const { execute, result, optimisticData } = useOptimisticAction(
addLikes,
{
currentData: { likesCount }, // gets passed from Server Component
updateFn: (prevData, { incrementBy }) => {
return {
likesCount: prevData.numOfLikes + amount
};
}
}
);

return (
<div>
<button
onClick={() => {
execute({ incrementBy: 3 });
}}>
Add 3 likes
</button>
{/* Optimistic state gets updated immediately, it doesn't wait for the server to respond. */}
<pre>Optimistic data: {optimisticData}</pre>

{/* Here's the actual server response. */}
<pre>Result: {JSON.stringify(result, null, 2)}</pre>
</div>
);
}

useOptimisticAction arguments

useOptimisticAction has the following arguments:

NameTypePurpose
safeActionFnHookSafeActionFnThis is the action that will be called when you use execute from hook's return object.
utils{ initData: Data; updateFn: (prevData: Data, input: InferIn<S>) => Data } & HookCallbacksObject with required initData, updateFn and optional callbacks. See below for more information.

utils properties in detail:

NameTypePurpose
currentDataData (return type of the safeActionFn you passed as first argument)An optimistic data setter. Usually this value comes from the parent Server Component.
updateFn(prevData: Data, input: InferIn<S>) => DataWhen you call the action via execute, this function determines how the optimistic data update is performed. Basically, here you define what happens immediately after execute is called, and before the actual result comes back from the server.
{ onExecute?, onSuccess?, onError?, onSettled? }HookCallbacksOptional callbacks. More information about them here.

useOptimisticAction return object

useOptimisticAction returns an object with the following properties:

NameTypePurpose
execute(input: InferIn<S>) => voidAn action caller with no return. The input is the same as the safe action you passed to the hook.
resultHookResultWhen the action gets called via execute, this is the result object.
statusHookActionStatusThe action current status.
reset() => voidYou can programmatically reset the result object with this function.
optimisticDataData (return type of the safeActionFn you passed as first argument)This is the data that gets updated immediately after execute is called, with the behavior you defined in the reducer function hook argument. The initial state is what you provided to the hook via initialOptimisticData argument.

Explore a working example here.