useActionState Patterns for Forms
Server Action:
'use server';
export async function submitForm(previousState, formData) {
const email = formData.get('email');
if (!email || !email.includes('@')) {
return { error: 'Invalid email address' };
}
await saveToDatabase({ email });
return { success: true };
}
**Component:**
```javascript
'use client';
import { useActionState } from 'react';
import { submitForm } from './actions';
function ContactForm() {
const [state, formAction, isPending] = useActionState(submitForm, null);
return (
<form action={formAction}>
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit'}
</button>
{state?.error && <p className="error">{state.error}</p>}
{state?.success && <p className="success">Submitted!</p>}
</form>
);
}
```
</workflow>
<conditional-workflows>
## Decision Points
**Progressive Enhancement:** Add permalink as third argument: `useActionState(submitForm, null, '/api/submit')`. Form submits to URL before JS loads; server handles both cases.
**Validation:** Server Action receives previousState, returns error object for failures, success object when valid; component renders errors from state.
**Multi-Step Forms:** Track step in state; Server Action advances step or returns errors; component renders current step.
</conditional-workflows>
<progressive-disclosure>
## References
- **Server Actions**: `../../forms/skills/server-actions/SKILL.md`
- **Form Validation**: `../../forms/skills/form-validation/SKILL.md`
- **Progressive Enhancement**: `../../../research/react-19-comprehensive.md` (lines 715-722)
**Cross-Plugin References:**
- If customizing validation error messages, use the customizing-errors skill for error formatting with safeParse and field error flattening
Load as needed for specific patterns.
</progressive-disclosure>
<examples>
## Example 1: Validation with
Zod
```javascript
'use server';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
message: z.string().min(10).max(1000),
});
export async function contactAction(previousState, formData) {
const data = {
email: formData.get('email'),
message: formData.get('message'),
};
const result = schema.safeParse(data);
if (!result.success) {
return { errors: result.error.flatten().fieldErrors };
}
try {
await db.contacts.create({ data: result.data });
return { success: true };
} catch (error) {
return { error: 'Failed to submit contact form' };
}
}
```
```javascript
'use client';
import { useActionState } from 'react';
import { contactAction } from './actions';
export default function ContactForm() {
const [state, formAction, isPending] = useActionState(contactAction, null);
if (state?.success) {
return <p>Thank you for contacting us!</p>;
}
return (
<form action={formAction}>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
{state?.errors?.email && <span className="error">{state.errors.email}</span>}
</div>
<div>
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required />
{state?.errors?.message && <span className="error">{state.errors.message}</span>}
</div>
<button type="submit" disabled={isPending}>
{isPending ? 'Sending...' : 'Send Message'}
</button>
{state?.error && <p className="error">{state.error}</p>}
</form>
);
}
```
## Example 2: Multi-Step Form
```javascript
'use server';
export async function multiStepAction(previousState, formData) {
const step = previousState?.step || 1;
if (step === 1) {
const name = formData.get('name');
if (!name || name.length < 2) {
return { step: 1, error: 'Name is required' };
}
return { step: 2, data: { name } };
}
if (step === 2) {
const email = formData.get('email');
if (!email?.includes('@')) {
return { step: 2, error: 'Valid email required', data: previousState.data };
}
await db.users.create({
data: { ...previousState.data, email },
});
return { step: 3, success: true };
}
}
```
```javascript
'use client';
import { useActionState } from 'react';
import { multiStepAction } from './actions';
export default function MultiStepForm() {
const [state, formAction, isPending] = useActionState(multiStepAction, { step: 1 });
if (state.success) {
return <p>Registration complete!</p>;
}
return (
<form action={formAction}>
{state.step === 1 && (
<>
<h2>Step 1: Name</h2>
<input name="name" type="text" required />
{state.error && <p className="error">{state.error}</p>}
</>
)}
{state.step === 2 && (
<>
<h2>Step 2: Email</h2>
<p>Name: {state.data.name}</p>
<input name="email" type="email" required />
{state.error && <p className="error">{state.error}</p>}
</>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Processing...' : state.step === 2 ? 'Complete' : 'Next'}
</button>
</form>
);
}
```
</examples>
<constraints>
**MUST**: First parameter
is `previousState`, second is `formData`; return serializable values (no functions, symbols); access form values via `formData.get('fieldName')`; mark functions with `'use server'` directive.
**SHOULD**: Validate inputs server-side; return structured error objects for field errors; disable submit button on `isPending`; show loading indicators; use validation libraries (zod, yup).
**NEVER**: Trust client-side validation alone; return sensitive data in errors; mutate `previousState` directly; skip error handling for async operations; omit authentication/authorization checks.
</constraints>
<validation>
**After Implementation**: Test form submission (valid data → success, invalid → errors, check `isPending`); verify Server Action (receives `previousState` and `formData`, returns serializable objects, handles errors); check security (server validates all inputs, authentication/authorization implemented, no sensitive data exposed); test progressive enhancement (disable JS, form submits to permalink, server handles both cases).
</validation>
---
## Common Patterns
**Optimistic Updates**: Combine with `useOptimistic` for immediate UI feedback:
```javascript
const [state, formAction] = useActionState(addTodo, null);
const [optimisticTodos, addOptimisticTodo] = useOptimistic(todos, (state, newTodo) => [
...state,
newTodo,
]);
```
See `../optimistic-updates/SKILL.md`.
**Reset Form on Success**:
```javascript
const formRef = useRef();
const [state, formAction] = useActionState(async (prev, formData) => {
const result = await submitForm(prev, formData);
if (result.success) {
formRef.current?.reset();
}
return result;
}, null);
return (
<form ref={formRef} action={formAction}>
...
</form>
);
```
For comprehensive documentation: `research/react-19-comprehensive.md` lines 135-180.
1---2name: using-action-state3description: Teaches useActionState hook for managing form state with Server Actions in React 19. Use when implementing forms, handling form submissions, tracking pending states, or working with Server Functions.4---5
6# useActionState Patterns for Forms
7
8<role>
9Teaches React 19's `useActionState` hook for form state management with Server Actions.
10</role>
11
12<when-to-activate>
13When: mentioning `useActionState`, form state/handling, Server Actions/Functions; tracking pending/submission status; implementing progressive enhancement; server-side form validation.
14</when-to-activate>
15
16<overview>
17`useActionState` manages form state based on action results: tracks pending state (automatic `isPending`), manages form state (returns action results), integrates Server Actions (`'use server'`), enables progressive enhancement (optional no-JS permalink). Replaces manual form submission state management.
18</overview>
19
20<workflow>
21## Standard Form with useActionState
22
23**Server Action:**
24
25```javascript
26'use server';
27
28export async function submitForm(previousState, formData) {
29 const email = formData.get('email');
30
31 if (!email || !email.includes('@')) {
32 return { error: 'Invalid email address' };
33 }
34
35 await saveToDatabase({ email });
36 return { success: true };
37}
38```
39
40````
41
42**Component:**
43
44```javascript
45'use client';
46
47import { useActionState } from 'react';
48import { submitForm } from './actions';
49
50function ContactForm() {
51 const [state, formAction, isPending] = useActionState(submitForm, null);
52
53 return (
54 <form action={formAction}>
55 <input name="email" type="email" required />
56
57 <button type="submit" disabled={isPending}>
58 {isPending ? 'Submitting...' : 'Submit'}
59 </button>
60
61 {state?.error && <p className="error">{state.error}</p>}
62 {state?.success && <p className="success">Submitted!</p>}
63 </form>
64 );
65}
66```
67
68</workflow>
69
70<conditional-workflows>
71## Decision Points
72
73**Progressive Enhancement:** Add permalink as third argument: `useActionState(submitForm, null, '/api/submit')`. Form submits to URL before JS loads; server handles both cases.
74
75**Validation:** Server Action receives previousState, returns error object for failures, success object when valid; component renders errors from state.
76
77**Multi-Step Forms:** Track step in state; Server Action advances step or returns errors; component renders current step.
78</conditional-workflows>
79
80<progressive-disclosure>
81## References
82
83- **Server Actions**: `../../forms/skills/server-actions/SKILL.md`
84- **Form Validation**: `../../forms/skills/form-validation/SKILL.md`
85- **Progressive Enhancement**: `../../../research/react-19-comprehensive.md` (lines 715-722)
86
87**Cross-Plugin References:**
88
89- If customizing validation error messages, use the customizing-errors skill for error formatting with safeParse and field error flattening
90
91Load as needed for specific patterns.
92</progressive-disclosure>
93
94<examples>
95## Example 1: Validation with
96
97Zod
98
99```javascript
100'use server';
101
102import { z } from 'zod';
103
104const schema = z.object({
105 email: z.string().email(),
106 message: z.string().min(10).max(1000),
107});
108
109export async function contactAction(previousState, formData) {
110 const data = {
111 email: formData.get('email'),
112 message: formData.get('message'),
113 };
114
115 const result = schema.safeParse(data);
116
117 if (!result.success) {
118 return { errors: result.error.flatten().fieldErrors };
119 }
120
121 try {
122 await db.contacts.create({ data: result.data });
123 return { success: true };
124 } catch (error) {
125 return { error: 'Failed to submit contact form' };
126 }
127}
128```
129
130```javascript
131'use client';
132
133import { useActionState } from 'react';
134import { contactAction } from './actions';
135
136export default function ContactForm() {
137 const [state, formAction, isPending] = useActionState(contactAction, null);
138
139 if (state?.success) {
140 return <p>Thank you for contacting us!</p>;
141 }
142
143 return (
144 <form action={formAction}>
145 <div>
146 <label htmlFor="email">Email</label>
147 <input id="email" name="email" type="email" required />
148 {state?.errors?.email && <span className="error">{state.errors.email}</span>}
149 </div>
150
151 <div>
152 <label htmlFor="message">Message</label>
153 <textarea id="message" name="message" required />
154 {state?.errors?.message && <span className="error">{state.errors.message}</span>}
155 </div>
156
157 <button type="submit" disabled={isPending}>
158 {isPending ? 'Sending...' : 'Send Message'}
159 </button>
160
161 {state?.error && <p className="error">{state.error}</p>}
162 </form>
163 );
164}
165```
166
167## Example 2: Multi-Step Form
168
169```javascript
170'use server';
171
172export async function multiStepAction(previousState, formData) {
173 const step = previousState?.step || 1;
174
175 if (step === 1) {
176 const name = formData.get('name');
177 if (!name || name.length < 2) {
178 return { step: 1, error: 'Name is required' };
179 }
180 return { step: 2, data: { name } };
181 }
182
183 if (step === 2) {
184 const email = formData.get('email');
185 if (!email?.includes('@')) {
186 return { step: 2, error: 'Valid email required', data: previousState.data };
187 }
188
189 await db.users.create({
190 data: { ...previousState.data, email },
191 });
192
193 return { step: 3, success: true };
194 }
195}
196```
197
198```javascript
199'use client';
200
201import { useActionState } from 'react';
202import { multiStepAction } from './actions';
203
204export default function MultiStepForm() {
205 const [state, formAction, isPending] = useActionState(multiStepAction, { step: 1 });
206
207 if (state.success) {
208 return <p>Registration complete!</p>;
209 }
210
211 return (
212 <form action={formAction}>
213 {state.step === 1 && (
214 <>
215 <h2>Step 1: Name</h2>
216 <input name="name" type="text" required />
217 {state.error && <p className="error">{state.error}</p>}
218 </>
219 )}
220
221 {state.step === 2 && (
222 <>
223 <h2>Step 2: Email</h2>
224 <p>Name: {state.data.name}</p>
225 <input name="email" type="email" required />
226 {state.error && <p className="error">{state.error}</p>}
227 </>
228 )}
229
230 <button type="submit" disabled={isPending}>
231 {isPending ? 'Processing...' : state.step === 2 ? 'Complete' : 'Next'}
232 </button>
233 </form>
234 );
235}
236```
237
238</examples>
239
240<constraints>
241**MUST**: First parameter
242
243is `previousState`, second is `formData`; return serializable values (no functions, symbols); access form values via `formData.get('fieldName')`; mark functions with `'use server'` directive.
244
245**SHOULD**: Validate inputs server-side; return structured error objects for field errors; disable submit button on `isPending`; show loading indicators; use validation libraries (zod, yup).
246
247**NEVER**: Trust client-side validation alone; return sensitive data in errors; mutate `previousState` directly; skip error handling for async operations; omit authentication/authorization checks.
248</constraints>
249
250<validation>
251**After Implementation**: Test form submission (valid data → success, invalid → errors, check `isPending`); verify Server Action (receives `previousState` and `formData`, returns serializable objects, handles errors); check security (server validates all inputs, authentication/authorization implemented, no sensitive data exposed); test progressive enhancement (disable JS, form submits to permalink, server handles both cases).
252</validation>
253
254---
255
256## Common Patterns
257
258**Optimistic Updates**: Combine with `useOptimistic` for immediate UI feedback:
259
260```javascript
261const [state, formAction] = useActionState(addTodo, null);
262const [optimisticTodos, addOptimisticTodo] = useOptimistic(todos, (state, newTodo) => [
263 ...state,
264 newTodo,
265]);
266```
267
268See `../optimistic-updates/SKILL.md`.
269
270**Reset Form on Success**:
271
272```javascript
273const formRef = useRef();
274
275const [state, formAction] = useActionState(async (prev, formData) => {
276 const result = await submitForm(prev, formData);
277 if (result.success) {
278 formRef.current?.reset();
279 }
280 return result;
281}, null);
282
283return (
284 <form ref={formRef} action={formAction}>
285 ...
286 </form>
287);
288```
289
290For comprehensive documentation: `research/react-19-comprehensive.md` lines 135-180.
291````