MENU
Combining with useOptimistic
useActionState() and useOptimistic() are commonly used together: useActionState() tracks the pending status and the committed result of an action across submissions, while useOptimistic() shows a value for that same data immediately, before the action has actually finished. Below, both wrap a single growing list of todos.
The form's action prop points at a small wrapper function. That wrapper first calls addOptimisticTodo() — safe here because the wrapper itself is already running inside the transition React wraps around a <form> action — and then calls and awaits submitAction(), the dispatch function returned by useActionState(), which runs the real (simulated) save and updates the committed todos list.
Add a todo — it appears instantly, labeled '(saving...)' via useOptimistic(), while useActionState() tracks isPending and threads the growing list through as state. After the simulated one-second save, the label disappears once the committed list includes the new item.
RESETRUNFULL
RESETRUNFULL
<!DOCTYPE html><html><head>
<script type="importmap">
{"imports": {"react": "https://esm.sh/react@19", "react-dom/client": "https://esm.sh/react-dom@19/client"}}
</script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head><body>
<div id="root"></div>
<script type="text/babel" data-type="module" data-presets="react">
import { useActionState, useOptimistic } from 'react';
import { createRoot } from 'react-dom/client';
let nextId = 3;
async function saveTodo(text) {
await new Promise((resolve) => setTimeout(resolve, 1000)); // simulate a slow server
return { id: nextId++, text };
}
const initialTodos = [
{ id: 1, text: 'Learn useActionState' },
{ id: 2, text: 'Learn useOptimistic' }
];
async function addTodoAction(previousTodos, formData) {
const newTodo = await saveTodo(formData.get('todo'));
return [...previousTodos, newTodo];
}
function App() {
const [todos, submitAction, isPending] = useActionState(addTodoAction, initialTodos);
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, newText) => [...currentTodos, { id: 'temp', text: newText + ' (saving...)' }]
);
async function formAction(formData) {
addOptimisticTodo(formData.get('todo')); // instant feedback
await submitAction(formData); // runs the real save, then updates 'todos'
}
return (
<div>
<ul>
{optimisticTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
<form action={formAction}>
<input type="text" name="todo" disabled={isPending} />
<button type="submit" disabled={isPending}>Add</button>
</form>
</div>
);
}
createRoot(document.getElementById('root')).render(<App />);
</script>
</body></html>