Form state preservation with useActionState, defaultValue pattern, create/edit reuse.
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.
'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.
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.
The same form component works for both creating and editing:
// Create - empty defaults, createPost action
<PostForm
action={createPost}
defaultValues={{ title: '', content: '', published:
The .bind(null, slug) pattern passes the slug as a curried first argument—the form data becomes the second argument.
Show validation errors contextually:
const [state, formAction] = useActionState(...);
const [error, setError] = useState<string | null>(null);
Place error messages near the relevant field or at the form level—wherever makes sense for your UI.