← Back
Edit Post
Title
Description
Form state preservation with useActionState, defaultValue pattern, create/edit reuse.
Content
Markdown supported
# useActionState Form submissions need to handle both the happy path and errors gracefully. When validation fails, losing all user input is one of the worst UX patterns—especially for long forms with rich content. `useActionState` solves this by preserving form state across submissions. The hook connects a Server Function to form state, maintaining values between renders. When an action returns error data, the form repopulates automatically. When it succeeds, you can redirect or reset. ## The Core Pattern ```tsx 'use client'; import { useActionState } from 'react'; import { useRouter } from 'next/navigation'; export function PostForm({ action, defaultValues, redirectTo }) { const router = useRouter(); const [state, formAction] = useActionState(async (_prev, formData) => { const result = await action(formData); if (result.success) { router.push(redirectTo); return _prev; // Keep previous state during redirect } // Return form data to repopulate inputs return result.formData ?? _prev; }, defaultValues); return ( <form action={formAction}> <Input name="title" defaultValue={state.title} /> <Textarea name="content" defaultValue={state.content} /> <SubmitButton>Save</SubmitButton> </form> ); } ``` The state flows: user submits → action runs → on error, state updates with returned data → inputs repopulate via `defaultValue`. ## Why defaultValue, Not value Using `defaultValue` instead of `value` is intentional. With `value`, you'd need `onChange` handlers and controlled components. With `defaultValue`, React handles the form natively—inputs are uncontrolled but repopulate when `state` changes. ## Reusing for Create and Edit The same form component works for both creating and editing: ```tsx // Create - empty defaults, createPost action <PostForm action={createPost} defaultValues={{ title: '', content: '', published: false }} redirectTo="/dashboard" /> // Edit - pre-filled with existing data, updatePost bound to slug <PostForm action={updatePost.bind(null, slug)} defaultValues={existingPost} redirectTo={`/dashboard/${slug}`} /> ``` The `.bind(null, slug)` pattern passes the slug as a curried first argument—the form data becomes the second argument. ## Error Display Show validation errors contextually: ```tsx const [state, formAction] = useActionState(...); const [error, setError] = useState<string | null>(null); // In the wrapper function: if (!result.success) { setError(result.error); return result.formData ?? _prev; } ``` Place error messages near the relevant field or at the form level—wherever makes sense for your UI.
Published
Save Changes
Cancel