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 undefined until 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 while state === '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

React
// 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>  );}
Angular
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);      },    });  }}
Angular
@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

TaskReactAngular
Read resultuseActionData()serverErrors signal set in error callback
Submitting stateuseNavigation().state === 'submitting'isSubmitting signal
Field errorsactionData.errors.titleerr.error.errors.title