Building a Multi-Page SPA with React RouterEntry 17 of 20
Validation Feedback with useActionData
If an action returns data instead of a redirect, useActionData() gives the component that data, typically server validation errors.
Notes
- Returns
undefineduntil the action runs at least once. - Typical shape from the server:
{ errors: { title: 'Invalid title.' } }. Render each error next to its field or as a list. - Combine with
useNavigation()to disable the submit button whilestate === 'submitting'. - Server validation is essential because browser validation can be bypassed in DevTools.
What / Why / When
- What
Reading the latest action result.
- Why
Shows the user what went wrong without leaving the form.
- When
Any form backed by server-side validation.
Code compare
// components/EventForm.tsximport { Form, useActionData, useNavigate, useNavigation } from "react-router";import type { SaveResult } from "../services/events";export default function EventForm({ method, event }: Props) { const actionData = useActionData() as SaveResult | undefined; const navigation = useNavigation(); const navigate = useNavigate(); const isSubmitting = navigation.state === "submitting"; return ( <Form method={method}> {actionData?.errors && ( <ul> {Object.values(actionData.errors).map((err) => ( <li key={err}>{err}</li> ))} </ul> )} {/* ...inputs from Entry 15 */} <button type="button" onClick={() => navigate("..")} disabled={isSubmitting}> Cancel </button> <button type="submit" disabled={isSubmitting}> {isSubmitting ? "Submitting..." : "Save"} </button> </Form> );}export class EventFormComponent { // ...form setup from Entry 15 protected isSubmitting = signal(false); protected serverErrors = signal<string[]>([]); onSubmit() { if (this.form.invalid) return; this.isSubmitting.set(true); this.eventsService.saveEvent(this.form.getRawValue(), this.event()?.id).subscribe({ next: () => this.router.navigate(["/events"]), error: (err: HttpErrorResponse) => { if (err.status === 422) this.serverErrors.set(Object.values(err.error.errors ?? {})); this.isSubmitting.set(false); }, }); }}@if (serverErrors().length) {<ul> @for (err of serverErrors(); track err) { <li>{{ err }}</li> }</ul>}<button type="submit" [disabled]="isSubmitting()"> {{ isSubmitting() ? 'Submitting...' : 'Save' }}</button>Quick reference
| Task | React | Angular |
|---|---|---|
| Read result | useActionData() | serverErrors signal set in error callback |
| Submitting state | useNavigation().state === 'submitting' | isSubmitting signal |
| Field errors | actionData.errors.title | err.error.errors.title |