This skill should be used when the user asks about "Server Actions", "form handling in Next.js", "mutations", "useFormState", "useFormStatus", "revalidatePath", "revalidateTag", or needs guidance on data mutations and form submissions in Next.js App Router.
/plugin marketplace add davepoon/buildwithclaude/plugin install nextjs-expert@buildwithclaudeThis skill inherits all available tools. When active, it can use any tool Claude has access to.
examples/mutation-patterns.mdreferences/form-handling.mdreferences/revalidation.mdServer Actions are asynchronous functions that execute on the server. They can be called from Client and Server Components for data mutations, form submissions, and other server-side operations.
Use the 'use server' directive inside an async function:
// app/page.tsx (Server Component)
export default function Page() {
async function createPost(formData: FormData) {
'use server'
const title = formData.get('title') as string
await db.post.create({ data: { title } })
}
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
)
}
Mark the entire file with 'use server':
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
await db.post.create({ data: { title } })
}
export async function deletePost(id: string) {
await db.post.delete({ where: { id } })
}
// app/actions.ts
'use server'
export async function submitContact(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
const message = formData.get('message') as string
await db.contact.create({
data: { name, email, message }
})
}
// app/contact/page.tsx
import { submitContact } from '@/app/actions'
export default function ContactPage() {
return (
<form action={submitContact}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
)
}
// app/actions.ts
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
export async function signup(formData: FormData) {
const parsed = schema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
})
if (!parsed.success) {
return { error: parsed.error.flatten() }
}
await createUser(parsed.data)
return { success: true }
}
Handle form state and errors:
// app/signup/page.tsx
'use client'
import { useFormState } from 'react-dom'
import { signup } from '@/app/actions'
const initialState = {
error: null,
success: false,
}
export default function SignupPage() {
const [state, formAction] = useFormState(signup, initialState)
return (
<form action={formAction}>
<input name="email" type="email" />
<input name="password" type="password" />
{state.error && (
<p className="text-red-500">{state.error}</p>
)}
<button type="submit">Sign Up</button>
</form>
)
}
// app/actions.ts
'use server'
export async function signup(prevState: any, formData: FormData) {
const email = formData.get('email') as string
if (!email.includes('@')) {
return { error: 'Invalid email', success: false }
}
await createUser({ email })
return { error: null, success: true }
}
Show loading states during submission:
// components/submit-button.tsx
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
)
}
// Usage in form
import { SubmitButton } from '@/components/submit-button'
export default function Form() {
return (
<form action={submitAction}>
<input name="title" />
<SubmitButton />
</form>
)
}
Revalidate a specific path:
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
await db.post.create({ data: { ... } })
// Revalidate the posts list page
revalidatePath('/posts')
// Revalidate a dynamic route
revalidatePath('/posts/[slug]', 'page')
// Revalidate all paths under /posts
revalidatePath('/posts', 'layout')
}
Revalidate by cache tag:
// Fetching with tags
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
})
// Server Action
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(formData: FormData) {
await db.post.create({ data: { ... } })
revalidateTag('posts')
}
'use server'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const post = await db.post.create({ data: { ... } })
// Redirect to the new post
redirect(`/posts/${post.slug}`)
}
Update UI immediately while action completes:
'use client'
import { useOptimistic } from 'react'
import { addTodo } from '@/app/actions'
export function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: string) => [
...state,
{ id: 'temp', title: newTodo, completed: false }
]
)
async function handleSubmit(formData: FormData) {
const title = formData.get('title') as string
addOptimisticTodo(title) // Update UI immediately
await addTodo(formData) // Server action
}
return (
<>
<form action={handleSubmit}>
<input name="title" />
<button>Add</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</>
)
}
Call Server Actions programmatically:
'use client'
import { deletePost } from '@/app/actions'
export function DeleteButton({ id }: { id: string }) {
return (
<button onClick={() => deletePost(id)}>
Delete
</button>
)
}
'use server'
export async function createPost(formData: FormData) {
try {
await db.post.create({ data: { ... } })
return { success: true }
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
return { error: 'A post with this title already exists' }
}
}
return { error: 'Failed to create post' }
}
}
'use server'
import { auth } from '@/lib/auth'
export async function deletePost(id: string) {
const session = await auth()
if (!session) {
throw new Error('Unauthorized')
}
const post = await db.post.findUnique({ where: { id } })
if (post.authorId !== session.user.id) {
throw new Error('Forbidden')
}
await db.post.delete({ where: { id } })
}
For detailed patterns, see:
references/form-handling.md - Advanced form patternsreferences/revalidation.md - Cache revalidation strategiesexamples/mutation-patterns.md - Complete mutation examplesThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.