#nextjs #useFormStatus #useActionState #hooks

Day 16 — Server Actions II: Loading & Error States

🧭 Day 16 — Server Actions II: Loading & Error States

Zero to Hero — Hands-on Next.js Tutorial

Yesterday, our Server Action was “fire and forget”. In reality, users need feedback:

  • “Loading…” while saving.
  • “Title is too short” if validation fails.

We need Client Hooks to solve this. Next.js provides useFormStatus and useActionState (formerly useFormState).


🟦 1. Pending State (useFormStatus)

To show a spinner while the action runs, use the useFormStatus hook. Catch: This hook must be used inside a component rendered within the <form>.

// SubmitButton.tsx
'use client';
import { useFormStatus } from 'react-dom';

export function SubmitButton() {
  const { pending } = useFormStatus();
  
  return (
    <button disabled={pending} className="bg-blue-500 disabled:bg-gray-400">
      {pending ? 'Saving...' : 'Save Post'}
    </button>
  );
}

// Page.tsx
export default function Page() {
  return (
    <form action={createPost}>
      <input name="title" />
      <SubmitButton /> {/* Must be a nested component! */}
    </form>
  )
}

🟩 2. Validation Errors (useActionState)

To return validation errors from the server, we need state. We swap from <form action={fn}> to const [state, dispatch] = useActionState(fn, initialState).

Server Action (actions.ts): Now accepts prevState as the first argument.

'use server';

export async function createPost(prevState: any, formData: FormData) {
  const title = formData.get('title');
  
  if (!title || title.length < 5) {
    return { message: 'Title is too short (min 5 chars)' };
  }
  
  await db.post.create({ ... });
  return { message: 'Success!' };
}

Client Form (page.tsx):

'use client';
import { useActionState } from 'react';
import { createPost } from './actions';

export function Form() {
  // state contains the return value of createPost
  // dispatch is the function to call (replaces action)
  const [state, dispatch] = useActionState(createPost, { message: '' });

  return (
    <form action={dispatch}>
      <input name="title" />
      <p className="text-red-500">{state?.message}</p>
      <button type="submit">Submit</button>
    </form>
  );
}

🟧 3. Zod Validation

In production, don’t validate manually using if statements. Use Zod.

npm install zod
import { z } from 'zod';

const schema = z.object({
  title: z.string().min(5),
  email: z.string().email()
});

export async function createPost(prevState: any, formData: FormData) {
  const validatedFields = schema.safeParse({
    title: formData.get('title'),
    email: formData.get('email'),
  });

  if (!validatedFields.success) {
    return { 
      errors: validatedFields.error.flatten().fieldErrors 
    };
  }
  // ... save db ...
}

🧪 Challenge: Day 16

  1. Create a “Subscribe to Newsletter” form.
  2. Use useActionState to handle the server response.
  3. Simulate a delay (await new Promise...) in the action.
  4. Use useFormStatus to disable the button during that delay.
  5. If sending “error@test.com”, return an error message to the UI.

See you tomorrow for Revalidating Data! ♻️