#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
- Create a “Subscribe to Newsletter” form.
- Use
useActionStateto handle the server response. - Simulate a delay (
await new Promise...) in the action. - Use
useFormStatusto disable the button during that delay. - If sending “error@test.com”, return an error message to the UI.
See you tomorrow for Revalidating Data! ♻️