What are Server Actions and the `"use server"` directive in React 19?
296 views
01
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
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
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
04
Explore the playground snippets
What 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.constACTION_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() { /* ... */ }`;
constCLIENT_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 ───────────────────────────────────────────constsave = async (formData) => {
awaitnewPromise((r) =>setTimeout(r, 500));
return formData.get("title");
};
exportdefaultfunctionApp() {
const [tab, setTab] = useState(0);
const [result, setResult] = useState(null);
constTABS = [["actions.js (server)", ACTION_FILE, "#f2f9f2"], ["PostRow.jsx (client)", CLIENT_FILE, "#eef"]];
return (
<divstyle={{padding:24, fontFamily: "system-ui", lineHeight:1.6, maxWidth:640 }}><divstyle={{display: "flex", gap:8, marginBottom:10 }}>
{TABS.map(([label], i) => (
<buttonkey={label}onClick={() => setTab(i)} style={{ fontWeight: i === tab ? "bold" : "normal" }}>
{label}
</button>
))}
</div><prestyle={{background:TABS[tab][2], border: "1pxsolid #ddd", borderRadius:8,
padding:12, fontSize:12, overflowX: "auto", whiteSpace: "pre-wrap" }}>
{TABS[tab][1]}
</pre><divstyle={{border: "1pxsolid #ddd", borderRadius:8, padding:12, marginTop:12 }}><strongstyle={{fontSize:13 }}>The calling convention, running for real</strong><formaction={async (fd) => { setResult("saved: " + JSON.stringify(await save(fd))); }}
style={{ marginTop: 6 }}
>
<inputname="title"defaultValue="a new post"style={{width: "100%", marginBottom:6 }} /><buttontype="submit">submit</button></form>
{result && <divstyle={{fontSize:13, color: "#161", marginTop:6 }}>{result}</div>}
<pstyle={{fontSize:12, color: "#666", margin: "6px00" }}>
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><pstyle={{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>
);
}