Server Functions

Let Client Components call async functions that execute on the server over the network.

Signature
async function createNoteAction(formData) {
  'use server';
  // runs on the server
}
  • Added in React 19.0

Parameters

ParameterTypeDescription
$...argsoptionalSerializableArguments 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

Example · jsx
// 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.

Example · jsx
// 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.

Example · jsx
// 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

  • Be the first to comment on this lesson.