Skip to the solution
HardFrontend7 min read

What are Server Actions and the "use server" directive in React 19?

297 views

The problem

Question presented to candidate: "What does the "use server" directive actually do to a function?"

What a strong answer should cover:

  • It marks a function as callable from the client but executed on the server. The bundler replaces the function body in the client bundle with a reference, and calling it performs a network request.
  • So it is RPC with a function-call syntax β€” the ergonomics of calling a function, the semantics of a POST.
  • The body never ships. That is the security property: a Server Action can read secrets and query the database because its code does not exist in the browser.
  • It is the inverse of "use client". One marks code that ships; the other marks code that never does.
  • Arguments and return values must be serialisable, because they are crossing a network.
  • Every Server Action is a public HTTP endpoint. Anyone can call it with any arguments. It must authenticate and validate exactly like an API route β€” the directive is not authorisation.
  • On the client it plugs into the same machinery as any Action: pass it to <form action>, wrap it with useActionState, read status with useFormStatus.
  • With a form it works before hydration, because the form can post to the endpoint natively.

Clarifying questions expected:

  • "Who is allowed to call this, and is that checked inside the action?" β€” the security question, and the important one.
  • "Is this called from a form or from an event handler?" β€” that decides progressive enhancement.

Code / implementation expected: Optional. The security point is what distinguishes a strong answer.

  • react
  • react-19
  • rsc
  • server-actions

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 8 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactWhat ships versus what does not, and the security checks that are not optional
import { useState } from "react";

// The directive needs a bundler and a server runtime, so this is an annotated
// reference. The live part below is the calling convention, which is identical
// whether the function is local or a Server Action.

const ACTION_FILE = `// app/actions.js
"use server";                         // every export here becomes an ENDPOINT

import { db } from "@/lib/db";
import { requireUser } from "@/lib/auth";

export async function deletePost(id) {
  //  1. AUTHENTICATE β€” the request may come from anyone at all
  const user = await requireUser();

  //  2. VALIDATE β€” id arrived over the network; it is attacker-controlled
  if (typeof id !== "string" || id.length > 40) throw new Error("Bad input");

  //  3. AUTHORISE β€” hiding the button in the UI protects nothing
  const post = await db.post.findUnique({ where: { id } });
  if (!post || post.authorId !== user.id) throw new Error("Forbidden");

  await db.post.delete({ where: { id } });
}

// ⚠️ This helper is exported from a "use server" module, so it is ALSO a
//    public endpoint β€” probably not what its author intended.
export async function internalRecalculateTotals() { /* ... */ }`;

const CLIENT_FILE = `// app/PostRow.jsx
"use client";
import { deletePost } from "./actions";   // imports a REFERENCE, not the body

export function PostRow({ post }) {
  // Looks like a function call. Is a POST to a generated endpoint.
  return <button onClick={() => deletePost(post.id)}>Delete</button>;
}`;

// ── Live: the calling convention ───────────────────────────────────────────
const save = async (formData) => {
  await new Promise((r) => setTimeout(r, 500));
  return formData.get("title");
};

export default function App() {
  const [tab, setTab] = useState(0);
  const [result, setResult] = useState(null);

  const TABS = [["actions.js (server)", ACTION_FILE, "#f2f9f2"], ["PostRow.jsx (client)", CLIENT_FILE, "#eef"]];

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 640 }}>
      <div style={{ display: "flex", gap: 8, marginBottom: 10 }}>
        {TABS.map(([label], i) => (
          <button key={label} onClick={() => setTab(i)} style={{ fontWeight: i === tab ? "bold" : "normal" }}>
            {label}
          </button>
        ))}
      </div>

      <pre style={{ background: TABS[tab][2], border: "1px solid #ddd", borderRadius: 8,
                    padding: 12, fontSize: 12, overflowX: "auto", whiteSpace: "pre-wrap" }}>
{TABS[tab][1]}
      </pre>

      <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, marginTop: 12 }}>
        <strong style={{ fontSize: 13 }}>The calling convention, running for real</strong>
        <form
          action={async (fd) => { setResult("saved: " + JSON.stringify(await save(fd))); }}
          style={{ marginTop: 6 }}
        >
          <input name="title" defaultValue="a new post" style={{ width: "100%", marginBottom: 6 }} />
          <button type="submit">submit</button>
        </form>
        {result && <div style={{ fontSize: 13, color: "#161", marginTop: 6 }}>{result}</div>}
        <p style={{ fontSize: 12, color: "#666", margin: "6px 0 0" }}>
          This action is local, but the shape is identical to a Server Action:
          the function receives <code>FormData</code> and React owns the pending
          state. Swapping in an imported <code>"use server"</code> function
          changes nothing on this side β€” which is the whole design.
        </p>
      </div>

      <p style={{ fontSize: 13, color: "#666" }}>
        The three numbered checks in the first tab are not defensive extras.
        The endpoint exists whether or not your UI ever renders a delete button,
        and anyone can post any id to it.
      </p>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 109 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

How do you safely handle async work and race conditions inside effects?

Continue