javascript 66 lines · 10 steps

Multi-step forms with useActionState in Next.js

A single Server Action drives a two-step signup wizard by returning the next step's state on each submit.

Explained by highlit
1'use client';
2 
3import { useActionState } from 'react';
4import { submitSignup } from './actions';
5 
6const initialState = {
7 step: 1,
8 values: { email: '', password: '', fullName: '', company: '' },
9 errors: {},
10 message: null,
11};
12 
13export function SignupForm() {
14 const [state, formAction, isPending] = useActionState(submitSignup, initialState);
15 const { step, values, errors } = state;
16 
17 return (
18 <form action={formAction} className="signup">
19 <input type="hidden" name="step" value={step} />
20 <ol className="progress">
21 <li className={step >= 1 ? 'active' : ''}>Account</li>
22 <li className={step >= 2 ? 'active' : ''}>Profile</li>
23 </ol>
24 
25 {step === 1 ? (
26 <fieldset disabled={isPending}>
27 <label>
28 Email
29 <input name="email" type="email" defaultValue={values.email} required />
30 </label>
31 {errors.email && <p className="error">{errors.email}</p>}
32 <label>
33 Password
34 <input name="password" type="password" defaultValue={values.password} required />
35 </label>
36 {errors.password && <p className="error">{errors.password}</p>}
37 <button type="submit">{isPending ? 'Checking…' : 'Continue'}</button>
38 </fieldset>
39 ) : (
40 <fieldset disabled={isPending}>
41 <input type="hidden" name="email" value={values.email} />
42 <input type="hidden" name="password" value={values.password} />
43 <label>
44 Full name
45 <input name="fullName" defaultValue={values.fullName} required />
46 </label>
47 {errors.fullName && <p className="error">{errors.fullName}</p>}
48 <label>
49 Company
50 <input name="company" defaultValue={values.company} />
51 </label>
52 <div className="actions">
53 <button type="submit" name="intent" value="back" formNoValidate>
54 Back
55 </button>
56 <button type="submit" name="intent" value="finish">
57 {isPending ? 'Creating account…' : 'Create account'}
58 </button>
59 </div>
60 </fieldset>
61 )}
62 
63 {state.message && <p className="notice">{state.message}</p>}
64 </form>
65 );
66}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1useActionState lets one Server Action own the entire form lifecycle, returning fresh state after each submission.
  2. 2Carrying prior values in hidden fields keeps a multi-step flow working without client-side state management.
  3. 3Deriving disabled and pending labels from isPending gives free loading feedback during the action.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Multi-step forms with useActionState in Next.js — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code