Server Actions remove the API route layer — form data goes directly from the
useActionState (React 19) provides pending state, form reset, and error handling for Server Actions — without it, users can double-submit
Progressive enhancement: the form works with JavaScript disabled (full-page POST) and enhances to client-side pending states when JS loads
Zod validation on the server prevents invalid data — mismatch between client and server validation causes confusing errors
revalidatePath and revalidateTag inside Server Actions update cached data without an API endpoint
Biggest mistake: calling a Server Action without useActionState and relying on the action's return value alone — no pending state means double submissions
✦ Definition~90s read
What is Server Actions?
Server Actions are async functions in Next.js that run on the server but can be called directly from client components, primarily via the <form action={}> attribute. They replace the traditional pattern of creating an API route, writing a fetch call, serializing JSON, and handling responses manually.
★
Server Actions are like mailing a letter directly from your mailbox to the recipient's kitchen table — no post office, no sorting center.
The framework handles serialization, transport, and response streaming — the developer writes a single function that receives FormData, validates it, performs database mutations, and returns the result.
The 'use server' directive marks a module or function as server-only. When placed at the top of a file, all exports become Server Actions. When placed inside an individual function, only that function runs on the server. Server Actions accept FormData (from forms) or plain arguments (when called via startTransition or from Server Components).
They can call revalidatePath() and revalidateTag() to invalidate cached data after mutations.
useActionState (React 19) is the hook that binds a Server Action to a form with automatic state management. It returns the action's return value (state), a pending flag, and a form action function. The pending flag enables the submit button to be disabled while the action is running, preventing double submissions — the most common production issue with Server Actions.
Without useActionState, each button click invokes the action independently, leading to duplicate mutations.
Server Actions support progressive enhancement out of the box. A <form action={serverAction}> works as a standard HTML form when JavaScript is disabled (full-page POST) and enhances to a partial page update when JavaScript loads. This means forms built with Server Actions work for all users regardless of JavaScript availability — a significant advantage over client-only form handling approaches.
Plain-English First
Server Actions are like mailing a letter directly from your mailbox to the recipient's kitchen table — no post office, no sorting center. The form in your browser sends data straight to a server function. useActionState is the 'processing' sticker on the envelope — it tells the sender not to mail a second copy because the first one is being handled. Without it, anxious users click 'Submit' twice, sending two payments. Zod validation is the mail sorter on the other end — it checks that every field is filled correctly before processing, rejecting bad letters before they cause damage.
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
Server Actions, stable since Next.js 15, eliminate the boilerplate of creating API routes for form submissions. You define an async function in your component file (or a separate file with 'use server'), and the <form action={}> attribute calls it directly. No fetch(), no JSON parsing, no route handler — the framework handles serialization and transport automatically.
The promise: one roundtrip from form to server to UI update. The reality: without useActionState to manage pending state, users double-submit forms, payments process twice, and error handling requires custom state management. Server Actions are powerful but they strip away the client-side UX safety rails that traditional API-route-based forms provided.
In this article, you'll learn the complete Server Actions pattern: how useActionState provides pending, error, and success states; how to validate with Zod on the server and surface errors back to the form; how progressive enhancement makes forms work without JavaScript; and the production patterns for payment forms, multi-step flows, and optimistic updates.
Server Actions: The One-Roundtrip Promise and Its Hidden Complexity
Server Actions eliminate the API route middle layer. Instead of creating a /api/checkout route, writing a fetch() call in the client, parsing JSON, and handling errors manually, you write a single async function decorated with 'use server'. The form's action attribute calls it directly. The framework handles serialization, transport, and response streaming.
This sounds magical. And it mostly is — for simple mutations. The complexity appears when you need: pending state (is the form submitting?), error state (what went wrong?), validation (are the fields valid?), optimistic updates (show the new data before the server confirms), and cache revalidation (update the page after mutation).
The key insight: Server Actions handle the transport layer. Everything else — state management, validation, error handling, cache invalidation — is your responsibility. The framework provides the tools (useActionState, revalidatePath, Zod integration), but you must wire them together correctly.
Server Action = API Route Without the Boilerplate
Server Actions replace fetch() + API routes with a single async function
useActionState provides pending, error, and success state — without it, you're blind to submission status
Zod validation must run on the server — client-only validation is bypassed by API callers
Cache revalidation (revalidatePath/revalidateTag) must be explicit — Server Actions don't auto-refresh
Production Insight
The double-submit incident happened because the team treated Server Actions as 'just an API route, but simpler.' They forgot that API routes had implicit request deduplication (the HTTP layer handles one request per connection). Server Actions don't have that — each invocation is independent. useActionState is not optional; it's the safety rail that prevents double submissions.
Rule: never use Server Actions without useActionState for any mutation with side effects (payments, writes, deletes).
Key Takeaway
Server Actions handle transport, not state management.
useActionState is required for pending/error/success states.
Without useActionState, double-submit is guaranteed for any multi-click scenario.
thecodeforge.io
Nextjs Server Actions Forms Guide
useActionState — The Missing Piece That Prevents Double Submissions
useActionState (useFormState in React 18) is a React 19 hook that binds a Server Action to a form with automatic state management. It returns the action's return value (state), a pending flag, a form action function, and a form reset function. The pending flag is the key: when true, you disable the submit button and show a loading indicator.
Before useActionState, teams used local state (useState or useReducer) to track submission status. This was error-prone — forgetting to set isSubmitting = false in an error handler left the form permanently disabled. useActionState handles all states automatically: idle, submitting, success, error.
The hook signature: const [state, formAction, isPending] = useActionState(action, initialState). The action function receives the previous state and FormData. The isPending boolean is true while the action is executing — use it to disable the submit button.
Generating the idempotency key inside the Server Action means every invocation gets a different key. Generate it on the client (before the action call) and pass it as a form field. The server checks if it has processed this key before before executing the mutation.
Production Insight
After the double-submit fix, the team's payment form went from 1,200 double charges per 3 hours to zero. useActionState's isPending flag disabled the submit button immediately on click. The client-generated idempotency key ensured that even if the button was programmatically re-enabled (via a bug), the second Server Action call would be idempotent.
Rule: useActionState is not optional for payment forms. It is the minimum viable safety measure. Add idempotency keys as a second layer.
Key Takeaway
useActionState provides pending state — disable submit button when state.pending is true.
Idempotency keys must be client-generated before the Server Action call.
Double-submit protection requires both useActionState and idempotency keys.
Zod Validation on the Server — Client Validation Is Not Enough
Client-side validation (HTML5 required, pattern attributes, or Zod in the browser) is for UX — instant feedback, no request needed. Server-side validation is for security — bad actors can bypass any client-side check by sending HTTP requests directly.
With Server Actions, Zod validation runs in the server function. Use safeParse() instead of parse() to avoid throwing on validation failure. Return field-level errors from the action so the form can display them next to the relevant inputs.
The pattern: define a shared Zod schema in a common file. Import it in both the client (for instant feedback) and the server (for security). The server always has the final say — if validation passes on the client but fails on the server, the server's errors override.
zod-validation.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// ============================================// Shared Schema: Used on Client and Server// ============================================import { z } from'zod'// ---- Shared schema ----exportconst checkoutSchema = z.object({
email: z
.string()
.email('Please enter a valid email address')
.min(1, 'Email is required'),
card: z
.string()
.regex(/^\d{16}$/, 'Card number must be 16 digits'),
cvv: z
.string()
.regex(/^\d{3,4}$/, 'CVV must be 3 or 4 digits'),
idempotencyKey: z.string().min(1, 'Invalid form session'),
})
exporttypeCheckoutInput = z.infer<typeof checkoutSchema>
// ---- Field error type for form display ----exporttypeZodFieldErrors = Partial<
Record<keyof CheckoutInput | 'form', string>
>
// ---- Server Action with Zod validation ----'use server'import { checkoutSchema, ZodFieldErrors } from'./schema'import { revalidateTag } from'next/cache'interfaceCheckoutState {
success: boolean
errors?: ZodFieldErrors
message?: string
}
exportasyncfunctioncheckout(
prevState: CheckoutState,
formData: FormData
): Promise<CheckoutState> {
// Validate with safeParse — no throwconst result = checkoutSchema.safeParse({
email: formData.get('email'),
card: formData.get('card'),
cvv: formData.get('cvv'),
idempotencyKey: formData.get('idempotencyKey'),
})
if (!result.success) {
// Map Zod errors to field namesconst fieldErrors: ZodFieldErrors = {}
for (const issue of result.error.issues) {
const field = issue.path[0] as keyof ZodFieldErrors
fieldErrors[field] = issue.message
}
return {
success: false,
errors: fieldErrors,
}
}
// Process payment (simplified)try {
const payment = awaitprocessPayment(result.data)
revalidateTag('orders')
return {
success: true,
message: payment.orderId,
}
} catch (error) {
return {
success: false,
errors: {
form: error instanceofError
? error.message
: 'Payment processing failed. Please try again.',
},
}
}
}
asyncfunctionprocessPayment(data: CheckoutInput) {
// ... payment gateway callreturn { orderId: 'ORD-123' }
}
Use Zod's safeParse() to return structured validation errors instead of throwing. A thrown ZodError in a Server Action results in a generic 500 error page — the user sees 'Something went wrong' with no details about which field failed. safeParse() gives you control over the error response.
Production Insight
A team using parse() instead of safeParse() in their Server Action had ZodErrors bubbling up as uncaught exceptions. Users saw 'Internal Server Error' on the checkout screen. The team spent two days debugging payment processing before realizing the issue was a malformed email field — caught by Zod but never surfaced to the user.
Rule: always use safeParse(). Format errors for the form. Never let validation exceptions escape the action.
Key Takeaway
Client-side validation is for UX — server-side validation with Zod is for security.
Use safeParse() to get structured errors without throwing.
Return field-level errors from the Server Action for inline form display.
With vs Without useActionState in PaymentsDouble-submit prevention in Next.js 16 server actionsWithout useActionStateWith useActionStateDouble-click preventionNo built-in guardButton disabled while pendingPending state trackingManual state managementAutomatic pending booleanIdempotency enforcementRequires custom logicIntegrated with action stateError handlingTry-catch in action onlyState-based error displayForm reset after successManual form resetAutomatic via state changeTHECODEFORGE.IO
thecodeforge.io
Nextjs Server Actions Forms Guide
Progressive Enhancement — Forms That Work Without JavaScript
Progressive enhancement means your form works without JavaScript. A user with JS disabled submits via a full-page POST. A user with JS enabled gets client-side pending states, inline validation, and partial page updates — all from the same Server Action.
Server Actions natively support progressive enhancement. The <form action={serverAction}> attribute works as a regular HTML form action when JavaScript is disabled — the browser serializes the form, POSTs to the server action URL, and the server responds with the rendered result. When JavaScript loads, next/router intercepts the form submission and calls the Server Action via fetch — no full-page reload.
The form must use standard HTML form elements (input, select, textarea, button) — not onClick handlers on divs. The action must accept FormData — not JSON. And the action must return a response that can be rendered as a full page (for JS-disabled users) or state (for JS-enabled users).
Progressive Enhancement = Graceful Degradation in Reverse
Use <form action={}> with standard HTML elements — no onClick handlers on divs
Accept FormData in the Server Action — the browser sends it automatically without JS
Return a response that renders as a full page (for JS-disabled) or state (for JS-enabled)
Test every form with JavaScript disabled to verify progressive enhancement works
Production Insight
A team building a SaaS app discovered 8% of their enterprise users had JavaScript disabled (corporate browser policies). Their Server Action forms stopped working entirely because they relied on onClick handlers. They rewrote to use <form action={serverAction}> — the forms worked for all users, with JS-enabled users getting enhanced UX on top.
Rule: test every Server Action form with JavaScript disabled. If it doesn't work, it's not progressively enhanced.
Key Takeaway
Progressive enhancement means the form works without JavaScript.
Use standard HTML form elements and FormData — not client-only event handlers.
Test with JS disabled to verify base functionality.
Revalidating Cache After Server Actions
Server Actions mutate data. But they do NOT automatically refresh the cached page. After a successful mutation, you must explicitly call revalidatePath() or revalidateTag() to tell Next.js that the cached data is stale.
This is a common source of bugs: the Server Action writes to the database successfully, the user sees a success message, but the page still shows old data because the cache wasn't invalidated. The user refreshes the page and sees the new data — confusing and unprofessional.
revalidatePath invalidates the Full Route Cache for a specific path. revalidateTag invalidates all Data Cache entries tagged with a given tag. Call whichever matches where the changed data appears. For a form that updates a product, call revalidatePath('/products/slug') and revalidateTag('products').
revalidation-after-action.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// ============================================// Cache Revalidation After Server Actions// ============================================'use server'import { revalidatePath, revalidateTag } from'next/cache'import { z } from'zod'import { db } from'@/lib/db'// ---- Update product title with cache invalidation ----const updateProductSchema = z.object({
slug: z.string().min(1),
title: z.string().min(1, 'Title is required').max(200),
})
interfaceUpdateState {
success: boolean
errors?: Record<string, string>
}
exportasyncfunctionupdateProduct(
prevState: UpdateState,
formData: FormData
): Promise<UpdateState> {
const result = updateProductSchema.safeParse({
slug: formData.get('slug'),
title: formData.get('title'),
})
if (!result.success) {
return { success: false, errors: { form: 'Invalid input' } }
}
await db.query(
'UPDATE products SET title = $1 WHERE slug = $2',
[result.data.title, result.data.slug]
)
// === CRITICAL: Invalidate cache ===revalidatePath(`/products/${result.data.slug}`) // Full Route CacherevalidateTag(`product-${result.data.slug}`) // Data CacherevalidateTag('products') // All product queriesreturn { success: true }
}
// ---- For list pages: revalidate the listing ----exportasyncfunctiondeleteProduct(
prevState: { success: boolean },
formData: FormData
) {
const slug = formData.get('slug') asstringawait db.query('DELETE FROM products WHERE slug = $1', [slug])
// Revalidate the list page and the product detail pagerevalidatePath('/products')
revalidatePath(`/products/${slug}`)
revalidateTag('products')
return { success: true }
}
No Auto-Revalidation — You Must Call It Explicitly
Server Actions do not automatically refresh the cache after mutation. If you forget to call revalidatePath or revalidateTag, the page continues serving cached data from before the mutation. The user sees a success message but unchanged data — a confusing failure state.
Production Insight
A team building a comment system had users posting comments successfully but the comment list didn't update until a manual page refresh. The Server Action inserted the comment into the database but forgot to call revalidatePath on the blog post page. The post page was served from cache — no new comment visible. Adding revalidatePath('/blog/slug') after the insert fixed it immediately.
Rule: call revalidatePath and revalidateTag after every Server Action mutation. Make it a checklist item for any Server Action you write.
Key Takeaway
Server Actions do NOT auto-revalidate cache — you must call revalidatePath or revalidateTag.
Match invalidation to the affected routes and data tags.
Make cache revalidation a checklist item for every Server Action.
Multi-Step Forms with Server Actions
Multi-step forms (wizards, checkout flows) with Server Actions require managing state across steps. Each step's data must be preserved between submissions until the final step completes.
The pattern: accumulate state in the useActionState return value. Each step submits to the same Server Action with a step identifier. The action processes the step, validates the data, and returns the accumulated state for the next step. The form renders different inputs based on the current step.
Alternative: store intermediate state in the database (partial draft) instead of in-memory state. This survives page refreshes and works without JavaScript. The trade-off: more database writes, but better resilience.
Passing accumulated state as hidden fields works for most cases but fails if the user refreshes mid-flow. For critical flows (checkout), save partial state to the database (a draft order) and pass a draft ID instead of the full JSON. This survives page refreshes and works without JavaScript.
Production Insight
A travel booking site had a 5-step checkout. Passing accumulated state via hidden fields worked until users refreshed mid-booking — all data was lost. They switched to saving a draft booking to the database after each step, passing only a draft ID. Users could refresh, close the tab and return, or even switch devices and continue from where they left off.
Rule: for multi-step forms, save partial state to the database. Hidden field accumulation is acceptable only for non-critical flows.
Key Takeaway
Multi-step Server Actions accumulate state across submissions.
Save partial state to the database for resilience — hidden field accumulation breaks on refresh.
Use a step identifier and accumulated data to track progress through the flow.
Error Handling Patterns for Server Actions
Server Actions throw errors differently than API routes. An uncaught error in a Server Action results in a Next.js error page — the user sees '500: Internal Server Error' with no actionable information. You must catch errors explicitly and return them as state.
The pattern: wrap the Server Action body in try/catch. Return errors as part of the state object. Use useActionState to read the error state in the form component and display it. Distinguish between validation errors (field-specific, user-fixable), business logic errors (e.g., 'Insufficient funds'), and system errors (generic 'try again later').
An uncaught error in a Server Action produces a generic error page. Every Server Action must be wrapped in try/catch. Return errors as structured state, not exceptions. The user should always see a helpful message, not a '500: Internal Server Error.'
Production Insight
A Server Action that didn't catch database connection errors returned the raw error message to the form: 'Connection refused: connect ECONNREFUSED 127.0.0.1:5432.' The user saw database connection details in their browser — a security concern and a terrible user experience. After implementing structured error handling, users saw: 'We encountered an error. Please try again.' — professional and safe.
Rule: never expose system error details to users. Always return user-friendly messages.
Key Takeaway
Every Server Action must be wrapped in try/catch.
Distinguish validation, business, and system error types.
Never expose system error details — always return user-friendly messages.
● Production incidentPOST-MORTEMseverity: high
Double-Submit Payments Due to Missing useActionState
Symptom
Users reported being charged twice for single orders. Support tickets spiked to 200 per hour. The payment provider log showed duplicate transaction IDs with identical timestamps within 200ms — clear double-submit pattern.
Assumption
The team assumed the Server Action's async execution would naturally prevent double submission — the function runs once per invocation, so clicking twice should call it twice but the payment gateway would deduplicate based on idempotency keys.
Root cause
The form had no pending state management. When users clicked 'Submit', the form immediately called the Server Action. If the user clicked again within 200ms (before the first request completed), a second Server Action invocation was fired. The Server Action was called twice, and the payment gateway processed both calls because the idempotency key was generated inside the action after the first call — both requests had different keys. The form had no useActionState to disable the submit button while pending.
Fix
1) Wrapped the Server Action with useActionState — the submit button is now disabled when state.pending is true. 2) Moved idempotency key generation to the client side (before the action call) using a unique session-based key. 3) Added server-side deduplication: if a payment with the same idempotency key already exists, return the existing result. 4) Added a 2-second debounce on the submit button regardless of useActionState. 5) Displayed a loading spinner with 'Processing payment — do not refresh' text to prevent user-induced double submits.
Key lesson
Server Actions do not prevent double invocation — you MUST use useActionState to disable the submit button while pending
Idempotency keys must be generated client-side before the action call — generating them inside the action means each call gets a different key
Debounce the submit button at the DOM level even with useActionState — it's a safety net for fast double-clickers
Always show pending state visually — users who don't see a loading indicator are more likely to click again
Production debug guideDiagnose common Server Action failures in production5 entries
Symptom · 01
Form submits but data doesn't update on the page
→
Fix
Check if the Server Action calls revalidatePath() or revalidateTag(). Server Actions update the database but do NOT automatically refresh the cache — you must explicitly revalidate the page or data after mutation.
Symptom · 02
Server Action throws an error but the form shows nothing
→
Fix
Check if the form uses useActionState or try/catch inside the action. Server Action errors must be caught and returned as state — uncaught errors result in a generic error page. Use useActionState to capture error state and display it in the form.
Symptom · 03
Form validation errors don't appear next to fields
→
Fix
Check if Zod validation errors are mapped to form fields. Return field-level errors from the Server Action as an object keyed by field name. Use the useActionState error state to display errors next to each input.
Symptom · 04
Server Action works locally but fails in production
→
Fix
Check environment variable access. Server Actions run in the server runtime — they have access to all .env variables. But if you're reading them inside a client component, they may not be available. Move env variable reads inside the Server Action.
Symptom · 05
Form takes too long to submit — no loading indicator
→
Fix
Verify the form uses useActionState. Without it, there is no built-in pending state. Add useActionState and use the pending state to show a loading spinner and disable the submit button.
★ Server Actions Quick Debug ReferenceFast commands for diagnosing Server Action issues
Wrap action body in try/catch, return error state instead of throwing
⚙ Quick Reference
6 commands from this guide
File
Command / Code
Purpose
use-action-state-form.tsx
'use client'
useActionState
zod-validation.ts
export const checkoutSchema = z.object({
Zod Validation on the Server
progressive-enhancement-form.tsx
'use client'
Progressive Enhancement
revalidation-after-action.ts
'use server'
Revalidating Cache After Server Actions
multi-step-form.tsx
'use client'
Multi-Step Forms with Server Actions
server-action-error-handling.ts
'use server'
Error Handling Patterns for Server Actions
Key takeaways
1
Server Actions eliminate API route boilerplate but require explicit state management with useActionState
2
useActionState provides pending state
always disable the submit button while isPending is true to prevent double submissions
3
Zod safeParse() on the server is required for security
client validation is UX-only and trivially bypassed
4
Cache revalidation (revalidatePath/revalidateTag) must be called explicitly after every Server Action mutation
5
Progressive enhancement ensures forms work without JavaScript
use <form action={}> with standard HTML elements
6
All Server Actions must be wrapped in try/catch
never let system errors reach the user
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Explain how Server Actions work in Next.js 16. How do they differ from t...
Q02SENIOR
How would you build a payment form with Server Actions that prevents dou...
Q03SENIOR
What is progressive enhancement in the context of Server Actions, and wh...
Q01 of 03SENIOR
Explain how Server Actions work in Next.js 16. How do they differ from traditional API routes?
ANSWER
Server Actions are async functions decorated with 'use server' that run on the server but are called directly from client components — no HTTP endpoint, no fetch call. The framework serializes the FormData, sends it to the server via a hidden POST endpoint, executes the function, and returns the result back to the client — all in one roundtrip. Unlike API routes, Server Actions integrate directly with React's transition system and can revalidate cache via revalidatePath and revalidateTag. They support progressive enhancement: a <form action={serverAction}> works as a full-page POST without JavaScript and enhances to a client-side SPA-like submission when JS loads. The main limitation: they can only be called from client components or server components using form actions — they're not HTTP endpoints consumable by third parties.
Q02 of 03SENIOR
How would you build a payment form with Server Actions that prevents double submissions?
ANSWER
1) Use useActionState to track pending state — disable the submit button when isPending is true. 2) Generate a unique idempotency key on the client before the action call (using cartId + timestamp), pass it as a hidden form field. 3) On the server, use Zod safeParse() to validate all fields. 4) Store processed idempotency keys in the database — if a key is already processed, return the existing result instead of charging again. 5) Add a DOM-level debounce on the submit button (2 seconds) as a third safety layer. 6) Show a 'Processing payment — do not refresh or click back' message with a loading spinner. 7) After success, call revalidateTag('orders') to update the order list.
Q03 of 03SENIOR
What is progressive enhancement in the context of Server Actions, and why does it matter?
ANSWER
Progressive enhancement means a form built with Server Actions works without JavaScript enabled. The <form action={serverAction}> attribute is a standard HTML form action — the browser serializes the form data and POSTs to the server action URL, receiving a full-page response. When JavaScript loads, the framework intercepts the submission, calls the Server Action via fetch, and updates the page with the returned state — no full reload. This matters because 3-8% of users have JavaScript disabled (corporate browsers, accessibility tools, low-power devices). These users are excluded if the form relies on JavaScript-only submission. Progressive enhancement ensures the form works for everyone, with enhanced UX for those with JavaScript enabled.
01
Explain how Server Actions work in Next.js 16. How do they differ from traditional API routes?
SENIOR
02
How would you build a payment form with Server Actions that prevents double submissions?
SENIOR
03
What is progressive enhancement in the context of Server Actions, and why does it matter?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
What is the difference between a Server Action and an API Route?
A Server Action is a function decorated with 'use server' that runs on the server and is called directly from a <form action={}> attribute or via startTransition. An API Route is a separate HTTP endpoint (e.g., /api/checkout) that you call with fetch(). Server Actions eliminate the HTTP boilerplate — no serialization, no route definition, no fetch call. However, API Routes are still needed for external webhooks, third-party integrations, and endpoints consumed by non-browser clients.
Was this helpful?
02
How do I prevent double submissions with Server Actions?
Use useActionState and disable the submit button when isPending is true. Additionally, generate a unique idempotency key on the client before the action call and pass it as a hidden form field. The server checks if it has already processed this key before executing the mutation. These two measures — client-side pending state and server-side deduplication — prevent double submissions completely.
Was this helpful?
03
Can Server Actions be called from non-form contexts?
Yes. Server Actions can be called from event handlers, useEffect, or any client code using startTransition: startTransition(() => serverAction(formData)). However, the progressive enhancement benefit is lost when called outside a form — a JavaScript-disabled browser cannot trigger the action. For critical flows, always prefer <form action={}> as the primary call site.
Was this helpful?
04
How do I handle file uploads with Server Actions?
Server Actions accept FormData, including file inputs. The file is sent as a FormData entry with the field name matching the input's name attribute. On the server, access it with formData.get('file') which returns a File object. Note that large files may hit request size limits — for uploads over 10MB, consider using dedicated API routes or direct-to-S3 uploads instead.
Was this helpful?
05
What is the difference between useActionState and useFormState?
useFormState is the React 18 version. useActionState is the React 19 version with a slightly different signature. In React 19 with Next.js 16, useActionState is preferred: const [state, formAction, isPending] = useActionState(action, initialState). The isPending flag is the key difference — useFormState required manual pending state tracking. useActionState includes it natively.