Skip to solution
hardFrontend

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

296 views
01

Understand 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.

reactreact-19rscserver-actions
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for senior React interviews — assumes Actions. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. **A n

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

What ships versus what does not, and the security checks that are not optional
Run Playground
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>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 109 of 119 decoded in the React.js track. One more won't hurt.

Back to track