Lesson 21 +10 XP

Form Handling & useActionState

Form Handling & useActionState

Handling form submission states, validation errors, and pending indicators in modern Next.js is powered by React's useActionState and useFormStatus hooks.

1. Managing Action State with useActionState

The useActionState hook manages asynchronous form state, returning validation messages or result objects from a Server Action.

// app/actions.ts
'use server';

export type FormState = {
  message: string;
  errors?: { email?: string; password?: string };
};

export async function loginUser(prevState: FormState, formData: FormData): Promise<FormState> {
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;

  if (!email.includes('@')) {
    return { message: 'Validation Failed', errors: { email: 'Invalid email address' } };
  }

  // Perform authentication...
  return { message: 'Successfully Logged In!' };
}

2. Client Form Component Implementation

// components/LoginForm.tsx
'use client';

import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
import { loginUser, FormState } from '@/app/actions';

const initialState: FormState = { message: '' };

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button
      type="submit"
      disabled={pending}
      className="w-full bg-blue-600 text-white py-2 rounded disabled:opacity-50"
    >
      {pending ? 'Logging in...' : 'Sign In'}
    </button>
  );
}

export default function LoginForm() {
  const [state, formAction] = useActionState(loginUser, initialState);

  return (
    <form action={formAction} className="max-w-md space-y-4">
      <div>
        <label className="block text-sm font-medium">Email</label>
        <input name="email" type="email" className="w-full border p-2 rounded" />
        {state.errors?.email && (
          <p className="text-red-500 text-xs mt-1">{state.errors.email}</p>
        )}
      </div>

      <SubmitButton />

      {state.message && <p className="text-sm font-bold">{state.message}</p>}
    </form>
  );
}

Important Rules for useFormStatus

⚠️ DocHero Safety Disclaimer

The useFormStatus hook must be called from a child component rendered inside the <form>. Calling it in the component containing the <form> tag will return pending = false!

Form Hook Comparison

Hook NameImport SourcePurpose
useActionState(action, initial)reactConnects action to component state & returns [state, formAction]
useFormStatus()react-domReturns { pending, data, method, action } status of parent form

TL;DR

  • useActionState connects Server Actions to local component state to display validation errors.
  • Action functions used with useActionState receive (prevState, formData) as arguments.
  • useFormStatus provides the pending state of a form for submit loading spinners.
  • useFormStatus must be placed inside a child component nested inside the target <form>.