--- title: React 19 Actions Pattern Reference --- # React 19 Actions Pattern Reference React 19 introduces **Actions** a pattern for handling async operations (like form submissions) with built-in loading states, error handling, and optimistic updates. This replaces the `useReducer + state` pattern with a simpler API. ## What are Actions? An **Action** is an async function that: - Can be called automatically when a form submits or button clicks - Runs with automatic loading/pending state - Updates the UI automatically when done - Works with Server Components for direct server mutation --- ## useActionState() `useActionState` is the client-side Action hook. It replaces `useReducer + useEffect` for form handling. ### React 18 Pattern ```jsx // React 18 form with useReducer + state: function Form() { const [state, dispatch] = useReducer( (state, action) => { switch (action.type) { case 'loading': return { ...state, loading: true, error: null }; case 'success': return { ...state, loading: false, data: action.data }; case 'error': return { ...state, loading: false, error: action.error }; } }, { loading: false, data: null, error: null } ); async function handleSubmit(e) { e.preventDefault(); dispatch({ type: 'loading' }); try { const result = await submitForm(new FormData(e.target)); dispatch({ type: 'success', data: result }); } catch (err) { dispatch({ type: 'error', error: err.message }); } } return (
); } ``` ### React 19 useActionState() Pattern ```jsx // React 19 same form with useActionState: import { useActionState } from 'react'; async function submitFormAction(prevState, formData) { // prevState = previous return value from this function // formData = FormData from ); } ``` **Differences:** - One hook instead of `useReducer` + logic - `formAction` replaces `onSubmit`, form automatically collects FormData - `isPending` is a boolean, no dispatch calls - Action function receives `(prevState, formData)` --- ## useFormStatus() `useFormStatus` is a **child component hook** that reads the pending state from the nearest form. It acts like a built-in `isPending` signal without prop drilling. ```jsx // React 18 must pass isPending as prop: function SubmitButton({ isPending }) { return ; } function Form({ isPending, formAction }) { return ( ); } // React 19 useFormStatus reads it automatically: function SubmitButton() { const { pending } = useFormStatus(); return ; } function Form() { const [state, formAction] = useActionState(submitFormAction, {}); return ( ); } ``` **Key point:** `useFormStatus` only works inside a ` > ); } ``` **Key points:** - `useOptimistic(currentState, updateFunction)` - `updateFunction` receives `(state, optimisticInput)` and returns new state - Call `addOptimistic(input)` to trigger the optimistic update - The server action's return value replaces the optimistic state when done --- ## Full Example: Todo List with All Hooks ```jsx import { useActionState, useFormStatus, useOptimistic } from 'react'; // Server action: async function addTodoAction(prevTodos, formData) { const text = formData.get('text'); if (!text) throw new Error('Text required'); const newTodo = await api.post('/todos', { text }); return [...prevTodos, newTodo]; } // Submit button with useFormStatus: function AddButton() { const { pending } = useFormStatus(); return ; } // Main component: function TodoApp({ initialTodos }) { const [optimistic, addOptimistic] = useOptimistic( initialTodos, (state, newTodo) => [...state, newTodo] ); const [todos, formAction] = useActionState( addTodoAction, initialTodos ); async function handleAddTodo(formData) { const text = formData.get('text'); // Optimistic: show it immediately addOptimistic({ id: Date.now(), text }); // Then submit the form (which updates when server confirms) await formAction(formData); } return ( <>