React Server Actions are functions you mark with the "use server" directive that run on the server and can be called directly from client components or HTML forms. They were introduced in React 19 (stable in late 2025) and are the standard form-submission mechanism in Next.js 15. For SEO teams measuring conversion rate, they matter because they let forms work before JavaScript hydrates, which on slow mobile networks is the difference between a lead captured and a lead lost.
This guide walks through what server actions do, why they lift conversion, the concrete implementation pattern for a high-intent form, how validation and error states fit together, and how to measure whether you actually got the lift you expected. The benchmarks we share come from production client work across SaaS and ecommerce lead forms in 2025 and 2026.
A server action is a function that lives on the server but can be invoked from the client via an action attribute on a form element or by being passed as a prop to a client component. Under the hood, React generates a serialized RPC (remote procedure call) endpoint and an associated client-side dispatch mechanism.
Here is the smallest possible example:
// app/contact/page.tsx
async function submitContact(formData: FormData) {
'use server'
const email = formData.get('email')
const message = formData.get('message')
await db.contacts.insert({ email, message })
redirect('/contact/thanks')
}
export default function ContactPage() {
return (
<form action={submitContact}>
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
)
}
Three things are happening here that are different from the traditional React pattern:
- The form has no
onSubmithandler. There is no client-side JavaScript intercepting the submission. - The action prop receives a function reference, but at the wire level the form submits with a regular HTTP POST to a React-generated endpoint.
- If JavaScript has not yet loaded or is disabled, the form still submits and works. The page reloads with the server response.
This last point is the conversion lever. In the old pattern, the form did nothing until React had downloaded, parsed, executed, and hydrated. On a slow mobile network with a 3-second hydration time, anyone who clicked submit before then got nothing. With server actions, the form is functional from the first paint.
For sites where high-intent forms drive revenue (lead capture, checkout, signup), this is the resilience pattern that has been missing in client-side React for years. It is also what HTML forms have done since 1994. Server actions are React rediscovering the web platform.
The conversion lift from server actions comes from four mechanical improvements, in roughly decreasing order of impact.
- Forms work pre-hydration. Hydration on a typical Next.js marketing page takes 800ms to 3 seconds on mobile. Any submission attempt before that window completes is lost in a traditional client-side form. With server actions, those submissions go through.
- Smaller JavaScript bundle. Validation logic, submission state machines, and fetch wrappers move server-side. The client bundle shrinks by 20kb to 80kb per form-heavy page, which improves LCP and INP.
- Faster Time to Interactive on form pages. Fewer event handlers to attach during hydration means less main-thread work, which improves INP (Interaction to Next Paint, the third Core Web Vital).
- Built-in progressive enhancement. The same form works in a browser, in a screen reader, in an old Android device, and to Googlebot's renderer. You stop debugging "why does the form work for me but not for the user."
We have seen this pattern produce conversion lifts in the 5% to 15% range on high-intent forms across client work. The biggest gains are on mobile traffic from slower geographies (Latin America, Southeast Asia, India), where hydration time is multiple seconds and the pre-hydration submission window catches a meaningful chunk of users.
The lift is smaller (sometimes zero) on forms that already had server-rendered alternatives (the old "do not break if JavaScript fails" pattern), but most React codebases dropped that discipline years ago. Server actions bring it back automatically.
For the broader Next.js architectural picture, our guide on server components versus client components in Next.js 15 covers where server actions fit alongside the rest of the rendering model.
Here is the production implementation pattern we use for a typical SaaS lead form. It handles validation, error states, success state, and supports JavaScript-disabled clients.
// app/(marketing)/demo/page.tsx
import { submitDemoRequest } from './actions'
export default function DemoPage() {
return (
<div>
<h1>Request a demo</h1>
<form action={submitDemoRequest}>
<label>
Work email
<input
name="email"
type="email"
required
autoComplete="email"
/>
</label>
<label>
Company
<input name="company" type="text" required />
</label>
<label>
Team size
<select name="size" required>
<option value="">Select</option>
<option value="1-10">1 to 10</option>
<option value="11-50">11 to 50</option>
<option value="51-200">51 to 200</option>
<option value="200+">200 plus</option>
</select>
</label>
<button type="submit">Request demo</button>
</form>
</div>
)
}
// app/(marketing)/demo/actions.ts
'use server'
import { z } from 'zod'
import { redirect } from 'next/navigation'
import { db } from '@/lib/db'
import { sendToSalesforce } from '@/lib/crm'
const DemoSchema = z.object({
email: z.string().email(),
company: z.string().min(2),
size: z.enum(['1-10', '11-50', '51-200', '200+']),
})
export async function submitDemoRequest(formData: FormData) {
const raw = Object.fromEntries(formData)
const parsed = DemoSchema.safeParse(raw)
if (!parsed.success) {
redirect('/demo?error=validation')
}
const lead = await db.leads.insert(parsed.data)
await sendToSalesforce(lead)
redirect('/demo/thanks')
}
What this pattern gets right:
- Server-side validation with Zod. The same schema can be reused for client-side preview validation if you want it, but the source of truth is the server. This eliminates a category of bugs where client and server validation drift apart.
- Redirect on success and error. Using
redirect()fromnext/navigationmakes the URL state reflect what happened, which is good for analytics, refresh-safety, and the back button. - Native HTML5 validation as the first line. The
required,type="email", and<select>constraints catch most bad input at the browser level, before the request even fires. - No JavaScript on the client for the happy path. This page can be a pure server component. Client JavaScript only ships if you add interactive components elsewhere on the page.
This pattern has shipped in production for several Capconvert clients running on our technical SEO audit recommended stack and consistently outperforms the equivalent client-side fetch implementation on form completion rate.
The trickiest part of server actions is handling validation errors gracefully without giving up the pre-hydration submission capability. Here is the layered strategy that works in practice.
Layer 1: Native HTML validation. Use required, type, pattern, min, max, minLength, and maxLength for everything the browser can check. This costs zero JavaScript and runs before the form submits.
Layer 2: Server-side validation in the action. Use Zod, Valibot, or hand-written validators to check the parsed form data. On failure, redirect back to the form page with an error query parameter or use the useFormState hook to surface error messages inline.
Layer 3 (optional): Client-side preview validation. If you want immediate feedback on invalid input before submission, add a client component that validates as the user types. This adds JavaScript but does not break the pre-hydration submission path.
The useFormState hook is the React 19 primitive for displaying validation errors without a full page reload:
'use client'
import { useFormState } from 'react-dom'
import { submitDemoRequest } from './actions'
const initialState = { errors: {} }
export function DemoForm() {
const [state, formAction] = useFormState(submitDemoRequest, initialState)
return (
<form action={formAction}>
<input name="email" />
{state.errors?.email && <p>{state.errors.email}</p>}
<button type="submit">Submit</button>
</form>
)
}
The trade-off is that useFormState requires the form to be a client component, which means it needs JavaScript to render the error inline. The fallback for pre-hydration submission is the redirect-with-query-string pattern.
A pragmatic rule: high-intent forms with simple validation use redirects (work pre-hydration). Complex multi-step forms with rich validation use useFormState (require JavaScript, which is acceptable for users deep enough in the funnel to have hydrated).
For users with JavaScript fully loaded, you can layer optimistic UI on top of the server action without breaking the pre-hydration fallback. React 19 ships useOptimistic for exactly this:
'use client'
import { useOptimistic } from 'react'
import { addItem } from './actions'
export function CartList({ items }) {
const [optimisticItems, addOptimistic] = useOptimistic(
items,
(state, newItem) => [...state, newItem]
)
async function handleSubmit(formData) {
const item = formData.get('item')
addOptimistic({ id: Date.now(), name: item })
await addItem(formData)
}
return (
<>
<ul>
{optimisticItems.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<form action={handleSubmit}>
<input name="item" />
<button>Add</button>
</form>
</>
)
}
The pattern shows the new item in the UI instantly (optimistically) while the server action runs in the background. If the action fails, React rolls back the optimistic state automatically.
For high-frequency interactions (cart updates, comment posts, likes), optimistic UI is the difference between feeling slow and feeling instant. It is not necessary for one-off lead forms, where the redirect-to-thanks-page pattern is already fast enough.
If you are migrating an existing client-side form to server actions and want to confirm the conversion lift, the cleanest experiment design is:
- Define your primary metric. Form completion rate (submissions divided by form views) is the right metric. Do not use raw submission count, because traffic changes confound it.
- Split traffic 50/50 between old and new. Use a feature flag (LaunchDarkly, GrowthBook, Vercel Edge Config) to route users to one version or the other.
- Run for at least two weeks. Conversion-rate experiments need adequate sample size, especially on weekly or bi-weekly traffic cycles.
- Segment by device, geography, and connection speed. The lift is typically larger on mobile, slow networks, and older devices. Aggregate numbers can hide where the real win is.
- Track downstream metrics. Did the increased submission rate also produce more qualified leads or revenue, or did it dilute lead quality? Sometimes faster forms attract more accidental submissions, which is a real but secondary effect.
In our client work, the typical breakdown looks like this:
| Segment | Old completion rate | New completion rate | Lift | |---|---|---|---| | Desktop, fast network | 24.1% | 24.8% | 2.9% | | Mobile, fast network | 18.4% | 20.2% | 9.8% | | Mobile, slow network | 11.2% | 13.7% | 22.3% | | Aggregate | 17.5% | 19.4% | 10.9% |
The aggregate lift is real but understates the mobile-slow-network gain, which is where the pre-hydration improvement does the most work.
Pair this with GA4 funnel reporting on landing-page to form-submission, and you can attribute the lift to the runtime change rather than to seasonal traffic variation. For the GA4 side, our content sites migration guide covers the analytics wiring that pairs well with conversion measurement.
A few things have bitten teams when adopting server actions:
- File uploads. Server actions support
FormDatawith files, but the default body size limit on Next.js is 1MB. For larger files, configureexperimental.serverActions.bodySizeLimitinnext.config.js. - Authentication context. Server actions run in a server context with access to cookies and headers via
cookies()andheaders()fromnext/headers. If you are reading auth from a client-side store, you need to pass it explicitly. - Caching and revalidation. After a successful action, call
revalidatePath()orrevalidateTag()to invalidate cached data. Otherwise the user might see stale data after their submission. - CSRF protection. Server actions are protected by Next.js with an Origin header check by default. If you are deploying behind a reverse proxy that strips Origin, configure
serverActions.allowedOriginsexplicitly. - Error boundaries. Errors thrown inside server actions become server errors. Use a try-catch in the action and return a typed error object if you want to surface them in the form UI instead of redirecting to an error page.
- Browser back button. After a redirect-on-success, the back button takes the user back to the form with their data lost. Use
redirect()with a status code or set a flash cookie to handle this gracefully.
These are all solvable but worth knowing before you ship. A pre-launch checklist that covers all six will save you a Sunday afternoon of incident response.
Are server actions slower than client-side fetch?
For the user-perceived submission time, they are usually faster because there is no separate JavaScript bundle to download, parse, and execute before the submission can fire. For the server-to-server hop, the work is identical (your action calls your database the same way a fetch handler would). The TTFB on the redirect response is what the user actually experiences.
Do server actions work with all React form libraries?
Most yes, but with caveats. React Hook Form, Formik, and other libraries that handle client-side state can wrap a server action. The libraries that did their own fetch handling (older versions of Formik, redux-form) need to be updated or replaced. For new code, the React 19 native useFormState plus useFormStatus is usually enough.
Can I use server actions outside Next.js?
Yes. Server actions are a React 19 feature, not a Next.js feature. They work in Remix, in Waku, and in any framework that implements the React Server Components RFC. Next.js was first to ship, so most examples online assume Next.js, but the underlying mechanism is portable.
Do server actions hurt my Core Web Vitals?
The opposite. They reduce client JavaScript, which improves INP and LCP. The only Core Web Vitals risk is if your server action is slow (long database query, slow third-party API call) and the form-submission TTFB drags on, which would show up in INP. Keep server actions fast (under 500ms ideally) and they only help.
How do server actions interact with edge runtime?
Server actions can run on Node serverless or on Vercel Edge depending on the route configuration. Edge is faster for cold starts but has a smaller API surface (no Node packages, no filesystem). For lead forms with simple database writes, edge is fine. For complex actions with heavy dependencies, Node serverless is the safer default.
Do server actions improve SEO directly?
Indirectly. Faster forms reduce bounce rate from form pages, which is a behavioral signal Google can pick up. The bigger SEO impact is the smaller JavaScript bundle and faster INP, which feed Core Web Vitals. Server actions are not a ranking factor themselves.
Should I migrate every existing form to server actions?
No. Low-value forms (newsletter signup, contact us with low traffic) are not worth the engineering time to migrate. High-value forms (demo request, signup, checkout) are worth migrating because the lift compounds with traffic. Prioritize by traffic times current conversion rate gap.
What is the easiest way to add server actions to an existing Next.js app?
Pick one high-value form. Move the submission handler from a client component fetch call to an action.ts file with "use server". Update the form to use the action prop instead of onSubmit. Deploy and measure. If the lift is real, do the next form. Iterate.
Are server actions tested differently?
Slightly. You can call server actions directly in unit tests since they are just async functions. You do not need to mock fetch or the API endpoint. For integration tests, use Playwright to test the full form submission with and without JavaScript disabled. That second case is the regression test that catches the pre-hydration scenario.
What about analytics events on form submission?
You can fire analytics events from inside the server action (server-side analytics) or from a useFormStatus hook on the client (client-side analytics). Server-side is more reliable (no ad blocker interference) but you lose some user context. Client-side requires hydration to have completed, which loses the pre-hydration submissions. The pragmatic answer is both, with deduplication on the analytics side.
Ready to optimize for the AI era?
Get a free AEO audit and discover how your brand shows up in AI-powered search.
Get Your Free Audit