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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1useActionState lets one Server Action own the entire form lifecycle, returning fresh state after each submission.
- 2Carrying prior values in hidden fields keeps a multi-step flow working without client-side state management.
- 3Deriving disabled and pending labels from isPending gives free loading feedback during the action.
Related explainers
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
javascript
import { useEffect, useRef, useState } from 'react'; export function useDelayedFlag(active, delay = 300) { const [visible, setVisible] = useState(false);
Delaying a loading spinner with a React hook
custom-hooks
debouncing
cleanup
Intermediate
8 steps
javascript
const SWIPE_THRESHOLD = 80; const MAX_TRANSLATE = 120; export function attachSwipeToDismiss(element, onDismiss) {
Building a swipe-to-dismiss gesture in JS
touch-events
gesture-detection
dom-manipulation
Intermediate
10 steps
javascript
const { pool } = require('./db'); function withTransaction() { return async (req, res, next) => {
A per-request transaction middleware in Express
middleware
database-transactions
connection-pooling
Advanced
7 steps
javascript
function collapseConsecutiveLogs(lines, { keyFn = (l) => l.message } = {}) { const groups = []; for (const line of lines) {
Collapsing consecutive log lines in JavaScript
grouping
run-length-encoding
data-transformation
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/multi-step-forms-with-useactionstate-in-next-js-explained-javascript-a9ab/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.