nextjs15_server_actions 22 Q&As

Nextjs15 Server Actions FAQ & Answers

22 expert Nextjs15 Server Actions answers researched from official documentation. Every answer cites authoritative sources you can verify.

unknown

22 questions
A

useActionState manages server action state and pending status: 'use client'; import { useActionState } from 'react'; const [state, action, pending] = useActionState(serverAction, initialState);

{state?.errors?.email &&

{state.errors.email}

}
. Server action signature: async function serverAction(prevState, formData) { const email = formData.get('email'); if (!email) return { errors: { email: 'Required' } }; return { success: true }; }. useActionState provides: state (returned from action), action (wrapped function for form), pending (boolean). Benefits: progressive enhancement (forms work without JS), built-in loading states, error handling. Works in both server and client components. Replaces deprecated useFormState. Production: combine with Zod for validation, return serializable objects only.

99% confidence
A

revalidatePath invalidates specific route path: import { revalidatePath } from 'next/cache'; await createPost(data); revalidatePath('/posts'). Invalidates all data on that page. Use for: single page updates, entire route segments. revalidateTag invalidates data by cache tag: import { revalidateTag } from 'next/cache'; await updateUser(id); revalidateTag('user-profile'). Requires fetch with tags: fetch(url, { next: { tags: ['user-profile'] } }). Use for: granular cache control, multi-page invalidation. Combine both: revalidatePath('/dashboard'); revalidateTag('stats'). Call before redirect() for fresh data: revalidatePath('/posts'); redirect('/posts'). Production: use revalidatePath for simple cases, revalidateTag for complex apps with shared data. New in Next.js 15: refresh() function refreshes router but doesn't revalidate tagged data.

99% confidence
A

Use Zod safeParse with error mapping: 'use server'; import { z } from 'zod'; const schema = z.object({ email: z.string().email(), password: z.string().min(8) }); export async function signUp(prevState, formData) { const parsed = schema.safeParse({ email: formData.get('email'), password: formData.get('password') }); if (!parsed.success) return { errors: parsed.error.flatten().fieldErrors }; const user = await db.insert(parsed.data); return { success: true, user }; }. Client: const [state, action] = useActionState(signUp, null); {state?.errors?.email && {state.errors.email[0]}}. Benefits: type safety, reusable schemas, field-level errors. Production: define shared schemas in separate file, use with react-hook-form via @hookform/resolvers/zod for client-side validation, sanitize data before database operations. Handle non-field errors separately: return { errors: {}, message: 'Server error' }.

99% confidence
A

Use redirect() from next/navigation after mutations: 'use server'; import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; export async function createPost(formData) { const post = await db.insert({ title: formData.get('title') }); revalidatePath('/posts'); redirect('/posts'); }. Important: call revalidatePath before redirect to ensure fresh data. redirect() throws error internally - place outside try/catch: try { await db.insert(data); } catch (error) { return { error: error.message }; } revalidatePath('/posts'); redirect('/posts'). Use permanentRedirect() for 308 (preserves POST method) vs redirect() for 303 (switches to GET). Production: redirect() prevents form resubmission on refresh (303 status). Known issue: Next.js 15.0.3 has redirect bugs in some cases - test thoroughly. Alternative: return success state and use router.push() in client component.

99% confidence
A

Use JavaScript bind() to create partial application: 'use client'; import { updateUser } from './actions'; export function UserProfile({ userId }) { const updateUserWithId = updateUser.bind(null, userId); return

; }. Server action receives bound args first: 'use server'; export async function updateUser(userId, formData) { const name = formData.get('name'); await db.users.update(userId, { name }); }. First bind() argument is context (use null), remaining arguments are prepended to action parameters. Benefits: works in server and client components, supports progressive enhancement, type-safe. Alternative: hidden inputs (value visible in HTML, not recommended for sensitive data). Use bind() for: IDs, user context, configuration. Production: combine with useActionState for state management: const [state, action] = useActionState(updateUser.bind(null, userId), null).

99% confidence
A

Use useOptimistic to update UI immediately before server response: 'use client'; import { useOptimistic } from 'react'; import { addTodo } from './actions'; export function TodoList({ todos }) { const [optimisticTodos, addOptimisticTodo] = useOptimistic(todos, (state, newTodo) => [...state, { id: Date.now(), text: newTodo, pending: true }]); async function handleSubmit(formData) { const text = formData.get('text'); addOptimisticTodo(text); await addTodo(text); }; return <>

    {optimisticTodos.map(t => <li style={{ opacity: t.pending ? 0.5 : 1 }}>{t.text})}
</>; }. useOptimistic returns [optimisticState, addOptimistic]. Second parameter is reducer updating optimistic state. Benefits: instant UI feedback, perceived performance boost, better UX. Production: show loading indicators for pending items, handle errors (optimistic state auto-reverts), works with useActionState for error handling.

99% confidence
A

Extract File from FormData and process: 'use server'; export async function uploadFile(formData: FormData) { const file = formData.get('file') as File; if (!file) return { error: 'No file uploaded' }; const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); await fs.writeFile(./uploads/${file.name}, buffer); return { success: true, filename: file.name }; }. Client form:

. Access file properties: file.name, file.size, file.type. For S3 upload: use AWS SDK with buffer. For large files (>100MB): use TUS protocol with chunking instead of server actions (avoids memory issues). Production: validate file type/size, scan for malware, generate unique filenames, store in cloud storage (S3/R2). Limit: 4.5MB per request. Use multipart/form-data encoding automatically handled by Next.js.

99% confidence
A

Next.js 15 made cookies() async - must await: 'use server'; import { cookies } from 'next/headers'; export async function setCookie() { const cookieStore = await cookies(); cookieStore.set('session', 'abc123', { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 60 * 60 * 24 * 7 }); }. Get cookie: const cookieStore = await cookies(); const session = cookieStore.get('session')?.value. Delete cookie: cookieStore.delete('session'). Options: httpOnly (prevents JS access), secure (HTTPS only), sameSite ('strict'|'lax'|'none'), maxAge (seconds). Cannot set cookies in Server Components (read-only) - only in Server Actions and Route Handlers. Migration from v14: remove await from cookies() calls using codemod. Production: use httpOnly + secure for auth tokens, set appropriate expiry, validate cookie values. All cookies are SameSite=lax by default for CSRF protection.

99% confidence
A

Import server action from separate file with 'use server': actions.ts: 'use server'; export async function createPost(formData: FormData) { const title = formData.get('title'); await db.insert({ title }); }. Client component: 'use client'; import { createPost } from './actions'; export function PostForm() { return

; }. Alternative: pass as prop from server component: . Client components cannot define server actions (must import). Call via: form action={serverAction}, event handler: onClick={() => serverAction()}, or with startTransition (disables progressive enhancement): startTransition(async () => { await serverAction(); }). Benefits: no API routes needed, type-safe with TypeScript. Production: wrap in useActionState for state management, handle loading states, validate inputs with Zod. Forms queue submissions if JS not loaded (progressive enhancement).

99% confidence
A

Call redirect() outside try/catch to avoid catching redirect error: 'use server'; export async function createPost(formData: FormData) { try { const post = await db.insert({ title: formData.get('title') }); } catch (error) { return { error: error.message }; } revalidatePath('/posts'); redirect('/posts'); }. redirect() throws error internally - placing inside try block catches it. Return errors as plain objects (Error instances not serializable): if (!valid) return { error: 'Validation failed', fields: { title: 'Required' } }. Use useActionState for client error handling: const [state, action] = useActionState(createPost, null); {state?.error &&

{state.error}

}. Unhandled errors don't return 500 (unlike API routes) - must catch explicitly. Production: use error boundaries for unexpected errors, Sentry for logging, return user-friendly messages. Validate all inputs with Zod before try block. Never expose internal error details to client.

99% confidence
A

Next.js 15 has built-in CSRF protection: validates Origin header matches Host header (or X-Forwarded-Host), only allows POST requests, uses SameSite cookies by default. Additional protection with allowedOrigins: // next.config.js: experimental: { serverActions: { allowedOrigins: ['myapp.com', '*.myapp.com'] } }. For CSRF tokens: use @nartix/next-csrf or @edge-csrf/nextjs packages. Rate limiting with Arcjet: import arcjet, { shield, rateLimit } from '@arcjet/next'; const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [shield(), rateLimit({ max: 10, window: '1m' })] }); 'use server'; export async function submitForm(formData) { const decision = await aj.protect(); if (decision.isDenied()) return { error: 'Rate limit exceeded' }; }. Production: always validate inputs with Zod, use authentication checks, sanitize data, set rate limits per user/IP, monitor for abuse. Server actions are security-sensitive - treat all input as hostile.

99% confidence
A

useActionState manages server action state and pending status: 'use client'; import { useActionState } from 'react'; const [state, action, pending] = useActionState(serverAction, initialState);

{state?.errors?.email &&

{state.errors.email}

}
. Server action signature: async function serverAction(prevState, formData) { const email = formData.get('email'); if (!email) return { errors: { email: 'Required' } }; return { success: true }; }. useActionState provides: state (returned from action), action (wrapped function for form), pending (boolean). Benefits: progressive enhancement (forms work without JS), built-in loading states, error handling. Works in both server and client components. Replaces deprecated useFormState. Production: combine with Zod for validation, return serializable objects only.

99% confidence
A

revalidatePath invalidates specific route path: import { revalidatePath } from 'next/cache'; await createPost(data); revalidatePath('/posts'). Invalidates all data on that page. Use for: single page updates, entire route segments. revalidateTag invalidates data by cache tag: import { revalidateTag } from 'next/cache'; await updateUser(id); revalidateTag('user-profile'). Requires fetch with tags: fetch(url, { next: { tags: ['user-profile'] } }). Use for: granular cache control, multi-page invalidation. Combine both: revalidatePath('/dashboard'); revalidateTag('stats'). Call before redirect() for fresh data: revalidatePath('/posts'); redirect('/posts'). Production: use revalidatePath for simple cases, revalidateTag for complex apps with shared data. New in Next.js 15: refresh() function refreshes router but doesn't revalidate tagged data.

99% confidence
A

Use Zod safeParse with error mapping: 'use server'; import { z } from 'zod'; const schema = z.object({ email: z.string().email(), password: z.string().min(8) }); export async function signUp(prevState, formData) { const parsed = schema.safeParse({ email: formData.get('email'), password: formData.get('password') }); if (!parsed.success) return { errors: parsed.error.flatten().fieldErrors }; const user = await db.insert(parsed.data); return { success: true, user }; }. Client: const [state, action] = useActionState(signUp, null); {state?.errors?.email && {state.errors.email[0]}}. Benefits: type safety, reusable schemas, field-level errors. Production: define shared schemas in separate file, use with react-hook-form via @hookform/resolvers/zod for client-side validation, sanitize data before database operations. Handle non-field errors separately: return { errors: {}, message: 'Server error' }.

99% confidence
A

Use redirect() from next/navigation after mutations: 'use server'; import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; export async function createPost(formData) { const post = await db.insert({ title: formData.get('title') }); revalidatePath('/posts'); redirect('/posts'); }. Important: call revalidatePath before redirect to ensure fresh data. redirect() throws error internally - place outside try/catch: try { await db.insert(data); } catch (error) { return { error: error.message }; } revalidatePath('/posts'); redirect('/posts'). Use permanentRedirect() for 308 (preserves POST method) vs redirect() for 303 (switches to GET). Production: redirect() prevents form resubmission on refresh (303 status). Known issue: Next.js 15.0.3 has redirect bugs in some cases - test thoroughly. Alternative: return success state and use router.push() in client component.

99% confidence
A

Use JavaScript bind() to create partial application: 'use client'; import { updateUser } from './actions'; export function UserProfile({ userId }) { const updateUserWithId = updateUser.bind(null, userId); return

; }. Server action receives bound args first: 'use server'; export async function updateUser(userId, formData) { const name = formData.get('name'); await db.users.update(userId, { name }); }. First bind() argument is context (use null), remaining arguments are prepended to action parameters. Benefits: works in server and client components, supports progressive enhancement, type-safe. Alternative: hidden inputs (value visible in HTML, not recommended for sensitive data). Use bind() for: IDs, user context, configuration. Production: combine with useActionState for state management: const [state, action] = useActionState(updateUser.bind(null, userId), null).

99% confidence
A

Use useOptimistic to update UI immediately before server response: 'use client'; import { useOptimistic } from 'react'; import { addTodo } from './actions'; export function TodoList({ todos }) { const [optimisticTodos, addOptimisticTodo] = useOptimistic(todos, (state, newTodo) => [...state, { id: Date.now(), text: newTodo, pending: true }]); async function handleSubmit(formData) { const text = formData.get('text'); addOptimisticTodo(text); await addTodo(text); }; return <>

    {optimisticTodos.map(t => <li style={{ opacity: t.pending ? 0.5 : 1 }}>{t.text})}
</>; }. useOptimistic returns [optimisticState, addOptimistic]. Second parameter is reducer updating optimistic state. Benefits: instant UI feedback, perceived performance boost, better UX. Production: show loading indicators for pending items, handle errors (optimistic state auto-reverts), works with useActionState for error handling.

99% confidence
A

Extract File from FormData and process: 'use server'; export async function uploadFile(formData: FormData) { const file = formData.get('file') as File; if (!file) return { error: 'No file uploaded' }; const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); await fs.writeFile(./uploads/${file.name}, buffer); return { success: true, filename: file.name }; }. Client form:

. Access file properties: file.name, file.size, file.type. For S3 upload: use AWS SDK with buffer. For large files (>100MB): use TUS protocol with chunking instead of server actions (avoids memory issues). Production: validate file type/size, scan for malware, generate unique filenames, store in cloud storage (S3/R2). Limit: 4.5MB per request. Use multipart/form-data encoding automatically handled by Next.js.

99% confidence
A

Next.js 15 made cookies() async - must await: 'use server'; import { cookies } from 'next/headers'; export async function setCookie() { const cookieStore = await cookies(); cookieStore.set('session', 'abc123', { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 60 * 60 * 24 * 7 }); }. Get cookie: const cookieStore = await cookies(); const session = cookieStore.get('session')?.value. Delete cookie: cookieStore.delete('session'). Options: httpOnly (prevents JS access), secure (HTTPS only), sameSite ('strict'|'lax'|'none'), maxAge (seconds). Cannot set cookies in Server Components (read-only) - only in Server Actions and Route Handlers. Migration from v14: remove await from cookies() calls using codemod. Production: use httpOnly + secure for auth tokens, set appropriate expiry, validate cookie values. All cookies are SameSite=lax by default for CSRF protection.

99% confidence
A

Import server action from separate file with 'use server': actions.ts: 'use server'; export async function createPost(formData: FormData) { const title = formData.get('title'); await db.insert({ title }); }. Client component: 'use client'; import { createPost } from './actions'; export function PostForm() { return

; }. Alternative: pass as prop from server component: . Client components cannot define server actions (must import). Call via: form action={serverAction}, event handler: onClick={() => serverAction()}, or with startTransition (disables progressive enhancement): startTransition(async () => { await serverAction(); }). Benefits: no API routes needed, type-safe with TypeScript. Production: wrap in useActionState for state management, handle loading states, validate inputs with Zod. Forms queue submissions if JS not loaded (progressive enhancement).

99% confidence
A

Call redirect() outside try/catch to avoid catching redirect error: 'use server'; export async function createPost(formData: FormData) { try { const post = await db.insert({ title: formData.get('title') }); } catch (error) { return { error: error.message }; } revalidatePath('/posts'); redirect('/posts'); }. redirect() throws error internally - placing inside try block catches it. Return errors as plain objects (Error instances not serializable): if (!valid) return { error: 'Validation failed', fields: { title: 'Required' } }. Use useActionState for client error handling: const [state, action] = useActionState(createPost, null); {state?.error &&

{state.error}

}. Unhandled errors don't return 500 (unlike API routes) - must catch explicitly. Production: use error boundaries for unexpected errors, Sentry for logging, return user-friendly messages. Validate all inputs with Zod before try block. Never expose internal error details to client.

99% confidence
A

Next.js 15 has built-in CSRF protection: validates Origin header matches Host header (or X-Forwarded-Host), only allows POST requests, uses SameSite cookies by default. Additional protection with allowedOrigins: // next.config.js: experimental: { serverActions: { allowedOrigins: ['myapp.com', '*.myapp.com'] } }. For CSRF tokens: use @nartix/next-csrf or @edge-csrf/nextjs packages. Rate limiting with Arcjet: import arcjet, { shield, rateLimit } from '@arcjet/next'; const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [shield(), rateLimit({ max: 10, window: '1m' })] }); 'use server'; export async function submitForm(formData) { const decision = await aj.protect(); if (decision.isDenied()) return { error: 'Rate limit exceeded' }; }. Production: always validate inputs with Zod, use authentication checks, sanitize data, set rate limits per user/IP, monitor for abuse. Server actions are security-sensitive - treat all input as hostile.

99% confidence