Next.js: Server Actions and Data Mutations
One of the most frequent form-handling questions in Next.js interviews is:
What are Server Actions in Next.js, and how do they support progressive enhancement? Explain how to validate schemas, clear caches, and update the UI optimistically.
Server Actions are asynchronous functions executed on the server that can be invoked directly from Client or Server Components. They form the backbone of forms, mutations, and database updates in the App Router.
1. What are Server Actions?
Server Actions are built on React's actions model. They replace conventional API routes (/pages/api/* or /app/api/route.ts) by allowing you to call server-side function endpoints inside event handlers or form actions directly.
"use server": Declares that a function or a module is server-only.- Client compatibility: Next.js compiles the action into a POST endpoint automatically under the hood.
// app/actions.ts
'use server';
export async function createFeedback(formData: FormData) {
const message = formData.get('message');
// Save to database
await db.feedback.create({ message });
}2. Progressive Enhancement
A key selling point of Server Actions is Progressive Enhancement:
- When invoked using the HTML
<form action={createFeedback}>attribute, the form works even if JavaScript is disabled on the client browser. - Once client-side hydration completes, Next.js intercepts the submit event to perform smooth, client-side fetch updates without reloading the page.
3. Form Validation & UI State
In real-world applications, you must validate incoming data and handle errors gracefully.
// app/actions.ts
'use server';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
});
export async function subscribeNewsletter(prevState: any, formData: FormData) {
const validation = schema.safeParse({
email: formData.get('email'),
});
if (!validation.success) {
return { error: 'Invalid email address' };
}
await db.subscribers.create({ email: validation.data.email });
return { success: true };
}In the Client Component:
You use the useActionState (formerly useFormState) hook to retrieve errors and submission statuses from Server Actions:
'use client';
import { useActionState } from 'react';
import { subscribeNewsletter } from './actions';
export default function SubscribeForm() {
const [state, formAction, isPending] = useActionState(subscribeNewsletter, null);
return (
<form action={formAction}>
<input type="email" name="email" required />
<button disabled={isPending}>Subscribe</button>
{state?.error && <p className="text-red-500">{state.error}</p>}
</form>
);
}4. Optimistic UI Updates
To improve perceived performance, you can display output changes instantly using React's useOptimistic hook before the server transaction finishes.
'use client';
import { useOptimistic } from 'react';
export function MessagesList({ initialMessages, sendMessageAction }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
initialMessages,
(state, newMessage: string) => [...state, { text: newMessage, sending: true }]
);
async function action(formData: FormData) {
const message = formData.get('message') as string;
addOptimisticMessage(message); // Show instantly
await sendMessageAction(message); // Send to server
}
return (
<form action={action}>
{optimisticMessages.map((m, idx) => (
<p key={idx} className={m.sending ? 'opacity-50' : ''}>{m.text}</p>
))}
<input name="message" />
</form>
);
}Key Takeaways
- useActionState: The standard pattern to handle action feedback loops (errors, messages) in interactive forms.
- Cache Revalidation: Always call
revalidatePath()orrevalidateTag()inside server actions to clear page caches immediately after performing mutations. - Security Check: Never assume client inputs are safe. Always validate schemas on the server using Zod or custom checks.
- Server-Only files: If a file defines export functions with
'use server', every function inside compiles into a public API endpoint.