mediumFull Stack EngineerTechnology
How would you implement optimistic UI updates in a Next.js application using Server Actions?
Posted 18/04/2026
by Mehedy Hasan Ador
Question Details
Interview question:
> "Our todo app feels slow because every action waits for the server response before updating the UI. How can we make it feel instant while keeping data consistent?"
> "Our todo app feels slow because every action waits for the server response before updating the UI. How can we make it feel instant while keeping data consistent?"
Suggested Solution
Optimistic Updates with useOptimistic
"use client";
import { useOptimistic } from "react";
import { createTodo, deleteTodo } from "./actions";
function TodoList({ todos: initialTodos }) {
const [optimisticTodos, addOptimistic] = useOptimistic(
initialTodos,
(state, newTodo) => [...state, { ...newTodo, id: "temp-" + Date.now(), pending: true }]
);
async function handleAdd(formData: FormData) {
const text = formData.get("text") as string;
addOptimistic({ text, completed: false });
await createTodo(formData); // Server Action
}
return (
<div>
<form action={handleAdd}>
<input name="text" placeholder="New todo..." />
<button type="submit">Add</button>
</form>
{optimisticTodos.map((todo) => (
<div key={todo.id} style={{ opacity: todo.pending ? 0.6 : 1 }}>
{todo.text}
</div>
))}
</div>
);
}
Server Action
// actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { prisma } from "@/app/lib/prisma";
export async function createTodo(formData: FormData) {
const text = formData.get("text") as string;
await prisma.todo.create({ data: { text } });
revalidatePath("/todos"); // Fresh data after mutation
}
export async function toggleTodo(id: string, completed: boolean) {
await prisma.todo.update({ where: { id }, data: { completed } });
revalidatePath("/todos");
}
The Flow
1. User clicks "Add" → UI instantly shows new item (optimistic)
2. Server Action runs → DB write + revalidation
3. Next.js fetches fresh data → UI updates with real data
4. If server fails → optimistic state rolls back automatically
Other Patterns
useActionState (for forms with validation feedback)
const [state, formAction, isPending] = useActionState(submitForm, initialState);
useFormStatus (inside form components)
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving..." : "Save"}</button>;
}
useOptimisticuseActionStateuseFormStatus