React Internals
Server Actions
Mutate data directly from components
Intermediate
react server forms nextjs
Definition
Server Actions are functions that run on the server but can be called directly from React components. They eliminate the need to create separate API routes for mutations, allowing forms and buttons to call server-side code seamlessly while handling progressive enhancement and error states automatically.
Basic Usage
Creating a Server Action
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
'use server';
const title = formData.get('title');
const content = formData.get('content');
// Server-side validation
if (!title || typeof title !== 'string') {
return { error: 'Title is required' };
}
// Database operation
await db.posts.create({
data: { title, content }
});
// Revalidate and redirect
revalidatePath('/posts');
redirect('/posts');
}
Using in Components
// app/posts/new/page.tsx
import { createPost } from '../actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input
name="title"
placeholder="Post title"
required
/>
<textarea
name="content"
placeholder="Write your post..."
/>
<button type="submit">Create Post</button>
</form>
);
}
Progressive Enhancement
Works Without JavaScript
<!-- Server renders form with action URL -->
<form action="/server-action" method="POST">
<input name="title" />
<button type="submit">Submit</button>
</form>
<!-- With JS: Intercept submit, call action -->
<!-- Without JS: Traditional form submission -->
JavaScript-Enhanced Experience
'use client';
import { useState } from 'react';
import { createPost } from './actions';
export default function PostForm() {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState('');
async function handleSubmit(formData: FormData) {
setIsPending(true);
setError('');
const result = await createPost(formData);
if (result?.error) {
setError(result.error);
}
setIsPending(false);
}
return (
<form action={handleSubmit}>
{error && <div className="error">{error}</div>}
<input name="title" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create'}
</button>
</form>
);
}
Advanced Patterns
Bound Actions
// Bind arguments to actions
async function updatePost(postId: string, formData: FormData) {
'use server';
await db.posts.update({
where: { id: postId },
data: { title: formData.get('title') }
});
revalidatePath(`/posts/${postId}`);
}
// Component usage
export default function EditPost({ post }) {
const updatePostWithId = updatePost.bind(null, post.id);
return (
<form action={updatePostWithId}>
<input
name="title"
defaultValue={post.title}
/>
<button type="submit">Update</button>
</form>
);
}
useFormStatus Hook
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
// Usage
export default function Form() {
return (
<form action={createPost}>
<input name="title" />
<SubmitButton />
</form>
);
}
Optimistic Updates
'use client';
import { useOptimistic } from 'react';
import { sendMessage } from './actions';
export default function Chat({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { ...newMessage, sending: true }]
);
async function handleSubmit(formData) {
const message = formData.get('message');
addOptimisticMessage({ text: message });
await sendMessage(message);
}
return (
<div>
{optimisticMessages.map((msg, i) => (
<div key={i} style={{ opacity: msg.sending ? 0.5 : 1 }}>
{msg.text}
</div>
))}
<form action={handleSubmit}>
<input name="message" />
<button>Send</button>
</form>
</div>
);
}
Error Handling
tryErrorBoundary
'use client';
import { useEffect } from 'react';
export default function ErrorFallback({ error, reset }) {
useEffect(() => {
// Log error
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
// page.tsx
import { ErrorBoundary } from 'react-error-boundary';
export default function Page() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<PostForm />
</ErrorBoundary>
);
}
Best Practices
1. Validate on Server
'use server';
import { z } from 'zod';
const schema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(10)
});
export async function createPost(formData: FormData) {
'use server';
const data = Object.fromEntries(formData);
const parsed = schema.safeParse(data);
if (!parsed.success) {
return {
error: 'Validation failed',
issues: parsed.error.issues
};
}
// ... create post
}
2. Revalidate After Mutations
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function updateUser(userId, formData) {
'use server';
await db.users.update({ where: { id: userId }, data: formData });
// Revalidate specific path
revalidatePath('/profile');
// Or revalidate by tag
revalidateTag(`user-${userId}`);
}
3. Security
'use server';
import { auth } from '@/lib/auth';
export async function deletePost(postId: string) {
'use server';
// Always check authentication
const session = await auth();
if (!session) {
throw new Error('Unauthorized');
}
// Check authorization
const post = await db.posts.findUnique({ where: { id: postId } });
if (post.authorId !== session.userId) {
throw new Error('Forbidden');
}
await db.posts.delete({ where: { id: postId } });
}
Key Takeaway
Server Actions simplify data mutations by eliminating API routes and enabling direct server calls from components. They support progressive enhancement (work without JS), provide built-in pending states, and integrate seamlessly with React’s form handling. Always validate on server, handle errors gracefully, and revalidate cached data after mutations.