Skip to main content
Chapter 4 Server Actions and Forms

Form Handling with useActionState

22 min read Lesson 14 / 28

Handling Form State with useActionState

useActionState (React 19, replacing useFormState) connects a Server Action to a Client Component, giving you access to the action's return value — error messages, success states, and field validation errors.

Server Action with State Return

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

interface ActionState {
    errors?: {
        email?: string[];
        password?: string[];
    };
    message?: string;
    success?: boolean;
}

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

    // Validate
    const errors: ActionState['errors'] = {};

    if (!email || !email.includes('@')) {
        errors.email = ['Please enter a valid email address.'];
    }

    if (!password || password.length < 8) {
        errors.password = ['Password must be at least 8 characters.'];
    }

    if (Object.keys(errors).length > 0) {
        return { errors };
    }

    // Attempt login
    const user = await verifyCredentials(email, password);

    if (!user) {
        return { message: 'Invalid email or password.' };
    }

    await createSession(user.id);

    return { success: true };
}

Client Component using useActionState

// app/(auth)/login/page.tsx
'use client';

import { useActionState } from 'react';
import { loginAction } from '@/actions/auth';

const initialState = {};

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

    return (
        <form action={formAction} className="max-w-sm mx-auto mt-16 space-y-4">
            <h1 className="text-2xl font-bold">Sign In</h1>

            {state.message && (
                <div className="bg-red-50 text-red-700 p-3 rounded-md">
                    {state.message}
                </div>
            )}

            <div>
                <label className="block text-sm font-medium mb-1">Email</label>
                <input
                    name="email"
                    type="email"
                    className="w-full border rounded-md px-3 py-2"
                />
                {state.errors?.email?.map(error => (
                    <p key={error} className="text-red-600 text-sm mt-1">{error}</p>
                ))}
            </div>

            <div>
                <label className="block text-sm font-medium mb-1">Password</label>
                <input
                    name="password"
                    type="password"
                    className="w-full border rounded-md px-3 py-2"
                />
                {state.errors?.password?.map(error => (
                    <p key={error} className="text-red-600 text-sm mt-1">{error}</p>
                ))}
            </div>

            <SubmitButton />
        </form>
    );
}

This pattern gives you server-side validation with per-field error messages without a single API route.