'use server'

Mark an async function, or a whole module, as callable by the client over the network.

Signature
async function addToCart(data) {
  'use server';
  // ...
}
  • Added in React 19.0

Return value

Nothing. It is a build-time marker that turns the function into a Server Function: the client gets a reference, and calling it makes a network request whose serialized return value is passed back.

Errors

Throws if applied to a non-async function, if used in a file that is not server-side, or if a caller passes an argument that cannot be serialized.

Changelog

  • PHP 19.0Directives became stable as part of the React Server Components model; previously Canary-only with an RSC-capable bundler.
  • PHP 19.0Functions so marked were called "Server Actions" until September 2024; the term is now reserved for ones passed to an action prop or called inside an action.

'use server' marks an async function as callable by the client. Put it as the first statement of a function body to mark that one function, or as the first line of a file to mark every export in that file — the module-level form is what allows Client Components to import the function directly.

When to reach for it

Reach for it when a browser interaction has to change server state: a form submit, a delete button, a toggle. The function is written where the data lives, and the round trip, serialization and error propagation are handled for you.

What trips people up

It marks functions, not components — there is no directive that makes something a Server Component. It only works on async functions, since the call is always a network request. And it must be the very first thing in the function body or file, in single or double quotes, never backticks.

The real hazard is security. Arguments are fully client-controlled, so treat them as untrusted input, validate them, and check that the signed-in user is allowed to perform the mutation inside the function. Call these functions in a Transition; <form action> and formAction do that automatically. They are for mutations, not data fetching — frameworks run one at a time and cannot cache the result.

Example

Example · jsx
// actions.js — module-level directive: every export is a Server Function
'use server';

import db from './db';
import { getSession } from './auth';

export async function addToCart(formData) {
  const session = await getSession();
  if (!session) throw new Error('Not signed in');

  // Arguments are client-controlled: validate before use.
  const productId = formData.get('productId');
  const quantity = Number(formData.get('quantity'));
  if (typeof productId !== 'string' || !Number.isInteger(quantity) || quantity < 1) {
    throw new Error('Invalid item');
  }

  await db.cart.add(session.userId, productId, quantity);
}

// AddToCart.jsx
'use client';
import { addToCart } from './actions';

export default function AddToCart() {
  return (
    <form action={addToCart}>
      <input type="hidden" name="productId" value="sku-42" />
      <label>
        Quantity <input name="quantity" type="number" defaultValue={1} min={1} />
      </label>
      <button type="submit">Add to cart</button>
    </form>
  );
}

When to use it

  • Handle a form submit that writes to the database, wired through form action.
  • Expose a delete or toggle mutation that a Client Component imports and calls in a transition.
  • Return a field-level validation error to useActionState instead of throwing.

Frequently asked questions

Does 'use server' make a component a Server Component?

No — it marks an async function as callable from the client, which is the opposite direction. Components outside a 'use client' subtree are already Server Components and need no directive at all.

Why does 'use server' only work on async functions?

Because calling one from the client is always a network request, and a synchronous function has no way to represent that latency. React rejects the directive on a non-async function at build time.

Is it safe to trust the arguments a Server Function receives?

No — arguments are fully client-controlled, including hidden form fields and ids that look like they came from your own server render. Validate every argument and re-check the signed-in user's permissions inside the function body.

More examples

Function-level directive, closed over server data

savePost is serialized to the client as a reference, not as source, and the closed-over post.id travels with it. The authorization check still runs on every invocation because the reference is a live endpoint: an attacker replaying it cannot change post.id, but they can call it while signed in as somebody else, which the authorId comparison rejects. On submit, useFormStatus flips the button to "Saving…" without any explicit transition, because form action wraps the call in one.

Example · jsx
// EditPost.jsx — a Server Component
import db from './db';
import { getSession } from './auth';
import SaveForm from './SaveForm';

export default async function EditPost() {
  const post = await db.posts.get('hello-world');

  async function savePost(formData) {
    'use server';
    // `post.id` is captured here, but the client can still invoke this
    // reference directly — so authorize again on every call.
    const session = await getSession();
    const current = await db.posts.get(post.id);
    if (!session || current.authorId !== session.userId) {
      throw new Error('Forbidden');
    }
    await db.posts.update(post.id, { body: String(formData.get('body') ?? '') });
  }

  return <SaveForm defaultBody={post.body} onSave={savePost} />;
}

// SaveForm.jsx
'use client';
import { useFormStatus } from 'react-dom';

function Submit() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? 'Saving…' : 'Save'}</button>;
}

export default function SaveForm({ defaultBody, onSave }) {
  return (
    <form action={onSave}>
      <textarea name="body" defaultValue={defaultBody} />
      <Submit />
    </form>
  );
}

Returning errors instead of throwing

Submitting "nope" renders "Enter a valid email address." under the form and leaves the input untouched; a valid new address renders the confirmation message and React resets the form. Throwing instead of returning would send the nearest error boundary a generic, digest-only message in production, which is why recoverable validation results are returned as data.

Example · jsx
// actions.js
'use server';
import db from './db';

export async function subscribe(prevState, formData) {
  const email = String(formData.get('email') ?? '');
  if (!email.includes('@')) {
    return { status: 'error', message: 'Enter a valid email address.' };
  }
  const existing = await db.subscribers.find(email);
  if (existing) {
    return { status: 'error', message: 'That address is already subscribed.' };
  }
  await db.subscribers.create(email);
  return { status: 'ok', message: 'Check your inbox to confirm.' };
}

// Subscribe.jsx
'use client';
import { useActionState } from 'react';
import { subscribe } from './actions';

export default function Subscribe() {
  const [state, formAction, isPending] = useActionState(subscribe, {
    status: 'idle',
    message: '',
  });

  return (
    <form action={formAction}>
      <input name="email" type="email" />
      <button disabled={isPending}>Subscribe</button>
      {state.status !== 'idle' && <p>{state.message}</p>}
    </form>
  );
}

Discussion

  • Be the first to comment on this lesson.