Preserving Form State with React's useActionState Hook
React recently introduced the useActionState hook, and while the official docs provide basic examples, I immediately wanted to tackle a common real-world scenario: preserving form input values after submission errors.
The Problem
When a user submits a form with invalid credentials, we typically want to:
- Show an error message
- Preserve what they’ve already typed (especially for multi-field forms)
Without this, users face the frustration of re-typing information they’ve already entered.
Traditional Approach
The traditional approach requires maintaining controlled components with onChange handlers:
function SignInForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const handleSubmit = async (e) => {
e.preventDefault()
try {
await signIn(email, password)
} catch (err) {
setError('Invalid credentials')
}
}
return (
<form onSubmit={handleSubmit}>
{error && <div className="error">{error}</div>}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Sign In</button>
</form>
)
}
The useActionState Approach
With useActionState, we can achieve the same result with less code and without tracking intermediate states:
'use server'
async function signInAction(prevState, formData) {
const email = formData.get('email')
const password = formData.get('password')
try {
await signIn(email, password)
return { success: true }
} catch (error) {
return {
email,
error: 'Invalid credentials'
}
}
}
import { useActionState } from 'react'
function SignInForm() {
const [state, formAction] = useActionState(signInAction, {
// initial state
})
if (state?.success) {
return <div>You're signed in!</div>
}
return (
<form action={formAction}>
{state?.error && <div className="error">{state.error}</div>}
<input
type="email"
name="email"
defaultValue={state?.email || ''}
required
/>
<input
type="password"
name="password"
required
/>
<button type="submit">Sign In</button>
</form>
)
}
The key part is to set the defaultValue on the input.
In particular, I’m a fan on letting the native components do its job. Historically, React (or people using React) used to track every piece of state - think of the state of input, kept in state and tracked with onChange. I’m happy with the paradigm shift.