Server Functions
Let Client Components call async functions that execute on the server over the network.
async function createNoteAction(formData) {
'use server';
// runs on the server
}- Added in React 19.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$...argsoptional | Serializable | Arguments are serialized and sent to the server: primitives, String/Array/Map/Set/TypedArray/ArrayBuffer, Date, FormData, plain objects, other Server Functions and Promises. React elements, classes, class instances, plain functions and DOM events are not supported. |
Return value
Promise<Serializable>A promise resolving to the function's return value, serialized on the server and deserialized on the client. Return values follow the same serializable-prop rules as a Client Component boundary.
Errors
Throws if a non-serializable argument is passed, if 'use server' is applied to a non-async function, or if the server-side call itself rejects — the rejection is surfaced on the client.
Changelog
- PHP 19.0Server Functions became stable and will not break between minor versions.
- PHP 19.0Renamed from "Server Actions" in September 2024. A Server Function is only a Server Action when it is passed to an action prop or called from inside an action.
- PHP 19.0useActionState replays form submissions entered before hydration finishes, and its third permalink argument enables redirects before JavaScript loads.
A Server Function is an async function marked with 'use server'. Your bundler replaces it, on the client, with a reference such as {$$typeof: Symbol.for("react.server.reference"), $$id: 'createNoteAction'}; calling that reference sends a network request carrying the serialized arguments, runs the real function on the server, and serializes the result back.
When to reach for it
Use one for mutations: creating a record, toggling a flag, deleting a row. They replace the usual hand-written POST endpoint plus fetch wrapper, and they compose with <form action> so a submit works before hydration finishes. They are explicitly not recommended for data fetching — frameworks typically process one action at a time and cannot cache the result. Fetch in a Server Component instead.
What trips people up
Arguments are fully client-controlled. The reference the browser holds is a live endpoint that anyone can call with any payload, so authenticate and authorize inside the function every time — a hidden form field or a closed-over id proves nothing.
Call them inside a Transition so pending state is tracked; <form action> and formAction do this for you. Calling one directly from an event handler without useTransition or useActionState leaves the UI with no pending state and unhandled rejections.
Example
// actions.js — every export in this file is a Server Function
'use server';
import db from './db';
import { getSession } from './auth';
export async function updateName(prevState, formData) {
const session = await getSession();
if (!session) return { error: 'Not signed in' };
const name = formData.get('name');
if (typeof name !== 'string' || name.trim() === '') {
return { error: 'Name is required' };
}
await db.users.update(session.userId, { name: name.trim() });
return { error: null, name: name.trim() };
}
// NameForm.jsx
'use client';
import { useActionState } from 'react';
import { updateName } from './actions';
export default function NameForm() {
const [state, submitAction, isPending] = useActionState(updateName, {
error: null,
});
return (
<form action={submitAction}>
<label>
Name <input name="name" defaultValue="Ada" />
</label>
<button type="submit" disabled={isPending}>
{isPending ? 'Saving…' : 'Save'}
</button>
{state.error && <p>{state.error}</p>}
{state.name && <p>Saved as {state.name}</p>}
</form>
);
}When to use it
- Submit a form that writes to the database without creating an API route.
- Toggle a favorite or delete a row from a button, with pending state from useTransition.
- Show a server-side validation error next to a form field using useActionState.
Frequently asked questions
What is the difference between a Server Function and a Server Action?
They are the same mechanism under two names: React renamed Server Actions to Server Functions in September 2024, and "Server Action" now refers only to a Server Function that is passed to an action prop or called from inside an action. Every Server Action is a Server Function, but a Server Function called directly from an event handler is not a Server Action.
Can I use a Server Function to fetch data?
It works, but React recommends against it — frameworks typically process one action at a time and provide no way to cache the return value, so parallel reads serialize and repeat. Fetch in a Server Component, and reserve Server Functions for mutations.
Why do I get "functions cannot be passed directly to Client Components"?
Because the function you passed as a prop was not marked with 'use server' and so has no server reference the client can call. Add the directive to the function body, or move the function into a module whose first line is 'use server'.
More examples
Defining a Server Function inside a Server Component
The browser receives no copy of deleteNote — onDelete arrives as a serialized function reference. Clicking Delete posts the id to the server, the ownership check runs there, and the button shows "Deleting…" for the duration because the call is wrapped in a transition. Note that the ownership check cannot be skipped by trusting the closure: a caller can invoke the reference with any id.
// page.jsx — a Server Component
import db from './db';
import { getSession } from './auth';
import DeleteButton from './DeleteButton';
export default async function NotesPage() {
const notes = await db.notes.getAll();
async function deleteNote(id) {
'use server';
const session = await getSession();
// Re-check on every call: `id` arrives from the client.
if (!session) throw new Error('Not signed in');
const note = await db.notes.get(id);
if (note.ownerId !== session.userId) throw new Error('Forbidden');
await db.notes.delete(id);
}
return (
<ul>
{notes.map((note) => (
<li key={note.id}>
{note.title} <DeleteButton id={note.id} onDelete={deleteNote} />
</li>
))}
</ul>
);
}
// DeleteButton.jsx
'use client';
import { useTransition } from 'react';
export default function DeleteButton({ id, onDelete }) {
const [isPending, startTransition] = useTransition();
return (
<button
disabled={isPending}
onClick={() => startTransition(() => onDelete(id))}
>
{isPending ? 'Deleting…' : 'Delete'}
</button>
);
}Progressive enhancement with a permalink
With JavaScript disabled or still loading, the form performs a plain browser POST and the user lands on /name/update, where the server renders the result. Once hydrated, the same submit is intercepted and handled without a navigation, and React replays any submission that happened during hydration so nothing is silently dropped.
// actions.js
'use server';
import db from './db';
export async function updateName(prevState, formData) {
const name = formData.get('name');
if (!name) return { error: 'Name is required' };
await db.users.updateCurrent({ name });
return { error: null };
}
// UpdateName.jsx
'use client';
import { useActionState } from 'react';
import { updateName } from './actions';
export default function UpdateName() {
// Third argument: the URL to navigate to if the form is submitted
// before this component has hydrated.
const [state, submitAction] = useActionState(
updateName,
{ error: null },
'/name/update'
);
return (
<form action={submitAction}>
<input name="name" />
<button type="submit">Update</button>
{state.error && <p>{state.error}</p>}
</form>
);
}
Discussion