Server Actions
Write Server Actions with "use server", call them from forms and clicks, see why they run one at a time, and why every action is a public endpoint.
- 01Write a Server ActionMark a file or one async function in a Server Component with "use server", and know where the directive can't go.
- 02Call one from a form or a clickPass it to a form that works before the JavaScript loads, or call it from a click inside a transition, with a pending state.
- 03Read what it sendsSpot an action's POST and its next-action header, explain why actions run one at a time, and check every input inside it.
A function the browser can call
The tea shop's Mint tea page lists its reviews and has a box to write a new one. Saving a review means changing data on the server. In a client-rendered React app you would write an API address for it and fetch it from the browser. In Next.js you write a function, mark it "use server", and hand it to the form. The function stays on the server, and the page calls it anyway. Your first Next.js app saved a note this way; here is the same idea for reviews, recorded from real Next.js 16.4.3 and real Chrome.
app/actions.ts"use server"; import { revalidatePath } from "next/cache";import { saveReview } from "./reviews"; export async function addReview(formData: FormData) { const text = String(formData.get("text") ?? "").trim(); if (text) await saveReview(text); revalidatePath("/");}Each step was taken in real Chrome against next start. Pick one to see the page after it.
- Cool and fresh.
- Lovely with honey.
- Smooth and calm.
- Focus
- the button “Post review”
- Scrolled
- 0 px down
- Announced to screen readers
Mint tea
Requests the page made during this step (1), without the files it loaded
POST/200text/x-component- next-router-state-tree
- %5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%2C49314%5D%7D%2Cnull%2Cnull%2C49234%5D
- next-action
- 40060689e296e335ce4040a5cf9094cdb10d60f823
Line 1 of app/actions.ts is "use server", a directive: a line of text that tells the build how to treat the code after it. Here it says that every function this file exports may be called from the browser. React calls such a function a Server Function, and one that a form or a transition calls to change data is a Server Action. Lines 6 to 10 are addReview. It gets the form's fields as formData, a FormData object. Line 7 reads the field named text, line 8 saves it, and line 9 tells Next.js that the stored HTML of / is out of date.
In app/page.tsx, line 14 hands the function to the form: <form action={addReview}>. The Steps tab is open on step 3, the click on Post review, after Chrome typed Smooth and calm. into the box. It made one request: a fetch, POST /, to the page's own address, with a next-action header. The answer is 200 with text/x-component. The review appeared in the list, the box is empty again, and the page never reloaded.
addReview as if it were in the browser. Next.js turns the call into a POST to the page, with the action's id in a next-action header. The function runs on the server, and the answer brings the updated page back in the same round trip.A café has a button on the counter for each drink. You press Mint tea; you never walk into the kitchen. The button sends an order number, the kitchen makes the tea, and the tea comes back with a receipt. The kitchen still checks every order it gets, because anyone can press the buttons.
- In real life: The button on the counter labeled Mint tea
- In Next.js: The form, or a button, that calls
addReview - In real life: The kitchen, where the tea is really made
- In Next.js: The server, where the action runs
- In real life: The order number the button sends to the kitchen
- In Next.js: The action's id, in the
next-actionheader - In real life: The tea and the receipt, handed back together
- In Next.js: The updated page, in one answer
A Server Action is an async function marked "use server" that runs on the server when the page calls it. A form calls it with its fields, and a click calls it inside a transition. Next.js turns each call into a POST request to the page, and the answer can bring back the updated page in the same round trip.
Writing a Server Action
"use server" goes in one of two places. At the very top of a file, before the imports, it makes every exported function an action, as in app/actions.ts. Or at the top of one async function's body, inside a Server Component: an inline action, which only that component can hand out. Try both, and two places where it doesn't work. Each button is one version of the review page, built with real Next.js. Before you click, guess which ones build.
"use server" in this placeNext.jsapp/actions.ts"use server"; import { revalidatePath } from "next/cache";import { saveReview } from "./reviews"; export async function addReview(formData: FormData) { const text = String(formData.get("text") ?? "").trim(); if (text) await saveReview(text); revalidatePath("/");}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (3/3) Finalizing page optimization ... Route (app)┌ ○ /└ ○ /_not-found ○ (Static) prerendered as static contentLine 1 marks every export of app/actions.ts as a Server Action. The page imports addReview, the build passes, and the Network tab's POST / saved a review with no JavaScript.
Two build and two don't:
- At the top of a file is the usual place. Server Components and Client Components can both import the actions, and one file can hold all of a feature's actions.
- Inside a Server Component, line 8 of
app/page.tsxstarts the body ofaddReview. An inline action can use the component's variables, like a tea's id; Next.js's docs say it encrypts the ones the action uses before they travel to the browser and back. - Inside a Client Component, the build stops: It is not allowed to define inline "use server" annotated Server Actions in Client Components. The error names the two fixes: import the action from a file that starts with
"use server", or pass it down as a prop from a Server Component, as Composing Server and Client Components did withaddAction. - On a function that isn't async, the build stops too: Server Actions must be async functions. A call from the browser is a network request, which always takes time, so the function has to return a promise.
Each row is one place for the directive, what it marks, and who can call what it marks:
| Where you write it | What it marks | Who can use it |
|---|---|---|
| At the top of a file, before the imports | Every function the file exports is a Server Action. | Any component: Server Components and Client Components can import them. |
| At the top of an async function's body, in a Server Component | That one function is a Server Action. | That Server Component, and the components it passes the function to as a prop. |
| At the top of a function in a Client Component | Nothing: the build stops with an error. | Nobody. Import the action from a "use server" file instead. |
| On a function that isn't async | Nothing: the build stops with an error. | Nobody. A call is a network request, so the function must be async. |
Components are Server Components unless a "use client" file says otherwise, so no directive makes a Server Component. "use client" marks where the browser's part of the app starts. "use server" marks functions that stay on the server but that the browser may call. Arguments and return values travel over the network, so they must be serializable; react.dev lists the types that can cross.
Calling one from a form
<form action={addReview}> is React's way to connect a form to a function (Form actions). When the visitor submits, React calls the function with the form's fields, in a transition. With a Server Action, the form also works before the page's JavaScript has loaded, or on a phone where it never does. Building a page so that it works as plain HTML first, and better once its JavaScript runs, is called progressive enhancement.
The recording shows both ways. Open the Network tab of the example above and pick /: the HTML's form says method="POST", with a hidden field named $ACTION_ID_ and a long code, which names the action. Then pick POST /, press Post review, before JavaScript: the recorder sent the form with Lovely with honey. the way a browser does with no JavaScript at all. Your first Next.js app's action ended with redirect() and got a 303. This one doesn't redirect, so the answer is the page itself: 200, a whole HTML page that already lists Lovely with honey., which the browser loads. The next request, / after posting, says x-nextjs-cache: MISS: line 9's revalidatePath made Next.js render the page again.
Each row compares the same press of Post review, before and after the JavaScript has loaded:
| Question | Before the JavaScript loads | After it loads |
|---|---|---|
| What does the browser send? | The form itself: POST /, with the text and a hidden field that names the action. | A fetch: POST /, with a next-action header that names the action. |
| What comes back? | A whole new HTML page, 200, with the new review in it. | An RSC payload, 200 text/x-component, with the updated page. |
| What does the visitor see? | The browser loads the new page. | The review appears in place. No page load. |
fetch with a next-action header, and the router puts the RSC payload on the page in place. Either way, addReview runs once, on the server.One more thing changed with JavaScript: the box is empty after step 3. After a form's action finishes without an error, React resets the form's uncontrolled fields, the ones whose value you don't keep in state yourself. A field you control with value and onChange keeps what's in your state. To show errors next to the box and keep what the visitor typed, the Forms lesson in this stage uses useActionState.
Calling one from a click
Not every change has a form. An Add to cart button sends only the tea's id. A Client Component can import an action from a "use server" file and call it from an event handler, like any async function:
app/add-button.tsx"use client"; import { useTransition } from "react";import { addToCart } from "./actions"; export default function AddButton({ id, name }: { id: string; name: string }) { const [isPending, startTransition] = useTransition(); function add() { startTransition(async () => { await addToCart(id); }); } return <button onClick={add}>{isPending ? `Adding ${name}…` : `Add ${name}`}</button>;}Each step was taken in real Chrome against next start. Pick one to see the page after it.
In your cart: nothing yet
- Focus
- the button “Adding Mint tea…”
- Scrolled
- 0 px down
Requests the page made during this step (1), without the files it loaded
POST/no answer yetfetch- next-router-state-tree
- %5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%2C514%5D%7D%2Cnull%2Cnull%2C530%5D
- next-action
- 40a9d7622a64e96fd85cd0272e1d38cd68e41c70f8
In app/add-button.tsx, line 4 imports addToCart from app/actions.ts. Line 7 calls useTransition, which gives back isPending and startTransition. Lines 9 to 13 are the click handler: it calls addToCart(id) inside startTransition. react.dev asks for this outside forms: a form puts the call in a transition for you, and a button doesn't. While the transition runs, isPending is true, and line 15 shows Adding Mint tea….
In app/actions.ts, addToCart prints a line, waits for addTea from app/cart.ts, prints another, and calls revalidatePath("/"). The stand-in database in app/cart.ts takes three seconds, so the wait is easy to see. The Steps tab is open on step 2, the click: the button says Adding Mint tea…, and Chrome sent one POST / with a next-action header that had no answer yet when the step ended. Step 3 waited for the cart: In your cart: Mint tea, and no new request. The answer to step 2's POST carried the updated page, because the action called revalidatePath. Next.js's docs call this a single round trip: the action's result and the new page in one response. The Terminal tab shows what next start printed: addToCart(mint) started, then addToCart(mint) finished.
The argument, "mint", travels in the request's body. Arguments and return values must be serializable, and only the id crosses here; addTea looks the tea up on the server. The docs also show an action called from an Effect, like counting a view once a page opens, inside startTransition as well.
Actions run one at a time
What if the visitor clicks Add Mint tea and then, before it's done, Add Green tea? The recorder did exactly that in real Chrome. Before you open the Steps tab, guess: does Chrome send the second POST right away?
app/actions.ts"use server"; import { revalidatePath } from "next/cache";import { addTea } from "./cart"; export async function addToCart(id: string) { console.log(`addToCart(${id}) started`); await addTea(id); console.log(`addToCart(${id}) finished`); revalidatePath("/");}Each step was taken in real Chrome against next start. Pick one to see the page after it.
In your cart: nothing yet
- Focus
- the button “Adding Green tea…”
- Scrolled
- 0 px down
No requests during this step: the page already had what it needed
Step 2 clicked Add Mint tea: one POST /, with no answer yet. Step 3, open now, clicked Add Green tea while the first was still running. Both buttons say Adding…, yet step 3 lists no request at all. Step 4 waited for both teas, and lists one POST /: the green one, sent after the mint one had answered. The Terminal tab agrees, line by line: addToCart(mint) started, addToCart(mint) finished, and only then addToCart(green) started and addToCart(green) finished.
addToCart("mint") is still running. Next.js holds it in line, so Chrome sends the second POST only after the first has answered. The stand-in database takes three seconds, so the whole wait is easy to see.That's by design. Next.js's docs say it dispatches Server Actions one at a time for each client: the second waits for the first to finish, so the page the router shows always matches the action that produced it. Three things follow:
- The server doesn't queue them. The line is in the browser's copy of the router. Each action is its own request, and the server runs whatever arrives, so two browser tabs, or two visitors, can run actions at the same time.
Promise.alldoesn't make them parallel. Calling two actions at once from the same page still sends them one after the other. For work that must happen together, do it inside one action.- Actions are for changes, not for reading. react.dev says Server Functions are designed for changes, not for loading data, and that frameworks run them one at a time and don't cache what they return. Read in Server Components, or from a Route Handler in the browser, as Fetching in the browser does.
In release v16.4.3, callServer in sequential-call-server.ts hands every action to the router's queue. dispatchAction, at lines 186 to 226 of app-router-instance.ts, starts it at once when the queue is empty and otherwise adds it to the end, to be started after the one before it finishes. The file's comments name an experimental option that swaps in a different queue; the docs describe the one-at-a-time default. The Under the hood stage reads this code.
What an action's request looks like
Every recording so far shows the same kind of request. Here are its parts, as far as the Steps and Network tabs show them. Each row is one part, with the JavaScript loaded and without it:
| Part | With JavaScript | Without JavaScript |
|---|---|---|
| The method and address | POST to the page's own address, /. | POST to the page's own address, /. |
| Which action to run | The next-action header. | A hidden field, $ACTION_ID_ followed by the id. |
| The arguments | In the request's body. | The form's fields, in the request's body. |
| The answer | text/x-component: an RSC payload for the router. | text/html: a page for the browser to show. |
- It's a
POSTto the page you're on. There's no address of its own. The docs say onlyPOSTcan call an action, and the recordings never show another method. - The
next-actionheader names the action with an id the build gave it. Both clicks in the last example sent the same id, because they called the same function; the tea's id was in the body. In the review recording, the form's hidden field and the header carry the same id. - The answer is an RSC payload,
text/x-component: the action's return value and, when the action changed what the page shows, the page rendered again. The request also carries anext-router-state-treeheader, the router's description of what's on the screen, which the server uses when it renders the page again.
How the body and the payload are written, and how the build makes the ids, belong to the Under the hood stage. One more answer is possible: when an action calls redirect(), the payload holds the new page and the router moves there, with status 200, as Redirects in Next.js recorded.
Every action is a public endpoint
An endpoint is an address on a server that anyone on the internet can send a request to. A Server Action is one, even though you never wrote an address. The recorder proves it: its POST / before JavaScript wasn't sent by a browser at all, only by a small program that copied the form's fields. Any program can do the same with any action whose id it has, and send whatever arguments it likes.
POST, with the action's id, runs the action too. So the action itself checks who's asking and what they sent.So the action itself is where the checks go. Next.js's docs list them:
- Check who's asking, and whether they may. Showing the form only to signed-in staff protects nothing, because the request can come without the form.
- Check every argument.
formDataand every other argument come from the visitor. Line 10 ofapp/cart.tsrefuses an id that isn't a tea, because the button isn't the only way to send one. - Return only what the page needs. Whatever an action returns travels to the browser.
Next.js adds a few protections of its own, which the docs describe: it compares the request's Origin header with the site's address and refuses actions sent from other sites' pages, it limits a request's body to 1 MB unless you change it, it makes action ids hard to guess and leaves out the ones no page uses, and it encrypts the variables an inline action captures. None of them knows who your visitor is. Securing Server Actions calls an action from outside the page and goes through each of them.
Planning the tea shop's actions
Each change in the shop needs a way to call its action. Forms suit anything with fields, and they work before the JavaScript loads. Buttons without fields call the action from a click, in a transition. And some things aren't changes at all. Sort these jobs:
- Post a review with a text box and a button.
- Add a tea to the cart from a button next to it.
- Show the tea's reviews on its page.
- Sign up for the newsletter with an email box.
- Count a view once the tea's page has opened.
- Show teas that match what the visitor is typing.
Sort each job: a form's action, an action called from a click or an Effect, or not an action at all.
The third group is the one people miss. An action can return data, but actions run one at a time and aren't meant for reading, so data that only needs to be shown comes from a Server Component, or from a Route Handler when the browser has to ask.
Common mix-ups
- "
"use server"makes a Server Component." No. Components are Server Components without any directive."use server"marks functions the browser may call. - "A Server Action has its own API address." No. It's a
POSTto the page it's called from, with anext-actionheader. - "Only my form can call my action." No. Anyone can send the same
POST. Check who's asking and what they sent inside the action. - "A form with an action needs JavaScript." No. The recorder posted the review form with none, and the review was saved.
- "Two quick clicks run two actions at once." No. Next.js sends the second after the first has answered, even with
Promise.all. - "Server Actions are a good way to load data." No. They're for changes. Read data in Server Components or from Route Handlers.
- "A Client Component can write its own action." No. It imports one from a
"use server"file, or gets one as a prop.
Practice exercises
These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.
The review form calls this addReview. Which HTTP method does pressing Post review use?
"use server";
import { revalidatePath } from "next/cache";
import { saveReview } from "./reviews";
export async function addReview(formData: FormData) {
const text = String(formData.get("text") ?? "").trim();
if (text) await saveReview(text);
revalidatePath("/");
}
POST. Pressing Post review sent one fetch, POST /, with a next-action header naming addReview, answered 200 text/x-component. Without JavaScript the form posts too.
A visitor on a slow train opens the Mint tea page. The page's JavaScript is still loading when they type a review and press Post review. Is the review saved?
Yes. The form is plain HTML with method="POST" and a hidden field that names the action. The recorder posted it with no JavaScript, and the answer, a whole page, lists the new review.
This review form is a Client Component, and the build stops at line 7. Where should addReview live instead?
"use client";
import { saveReview } from "./reviews";
export default function ReviewForm() {
async function addReview(formData: FormData) {
"use server";
await saveReview(String(formData.get("text") ?? ""));
}
return (
<form action={addReview}>
<input name="text" aria-label="Review" />
<button>Post review</button>
</form>
);
}
"use client";
import { addReview } from "./actions";
export default function ReviewForm() {
return (
<form action={addReview}>
<input name="text" aria-label="Review" />
<button>Post review</button>
</form>
);
}
Move addReview to app/actions.ts, which starts with "use server", and import it. The recorded build passes, and even this Client Component's form posted with no JavaScript: the HTML has the hidden field, and the answer lists the new review.
This button saves the tea, but for three seconds nothing on the page changes, and visitors press it again. Which React hook lets it say Adding Mint tea… while the action runs?
"use client";
import { addToCart } from "./actions";
export default function AddButton({ id, name }: { id: string; name: string }) {
return <button onClick={() => addToCart(id)}>Add {name}</button>;
}
"use client";
import { useTransition } from "react";
import { addToCart } from "./actions";
export default function AddButton({ id, name }: { id: string; name: string }) {
const [isPending, startTransition] = useTransition();
function add() {
startTransition(async () => {
await addToCart(id);
});
}
return <button onClick={add}>{isPending ? `Adding ${name}…` : `Add ${name}`}</button>;
}
useTransition. Line 7 gets isPending and startTransition; lines 9 to 13 call the action inside the transition; line 15 shows Adding Mint tea… while it runs. The recording's step 2 shows that text.
Add both calls addToCart twice at once, with Promise.all. Each action prints when it starts and finishes. Which line does the Terminal print right after addToCart(mint) started?
"use client";
import { useTransition } from "react";
import { addToCart } from "./actions";
export default function AddBoth() {
const [isPending, startTransition] = useTransition();
function addBoth() {
startTransition(async () => {
await Promise.all([addToCart("mint"), addToCart("green")]);
});
}
return <button onClick={addBoth}>{isPending ? "Adding both…" : "Add both"}</button>;
}
addToCart(mint) finished. Both calls start at once in the browser, but Next.js sends them one at a time: the recorded Terminal prints mint started, mint finished, green started, green finished, and the click's step lists two POST / requests.
With this app/actions.ts, next build stops. Which word is missing?
"use server";
import { revalidatePath } from "next/cache";
import { saveReview } from "./reviews";
export function addReview(formData: FormData) {
const text = String(formData.get("text") ?? "").trim();
if (text) saveReview(text);
revalidatePath("/");
}
"use server";
import { revalidatePath } from "next/cache";
import { saveReview } from "./reviews";
export async function addReview(formData: FormData) {
const text = String(formData.get("text") ?? "").trim();
if (text) await saveReview(text);
revalidatePath("/");
}
async, on line 6: export async function addReview. The recorded build stops with: Server Actions must be async functions. With async, and await before saveReview, it builds.
Check your understanding
For each question, ask where the function runs, and what travels between the browser and the server.
Question 1 of 7What does
"use server"at the top ofapp/actions.tsdo?Choose an answer to see the explanation.
Question 2 of 7Before the page's JavaScript loads, a visitor posts a review.
addReviewdoesn't redirect. What comes back?Choose an answer to see the explanation.
Question 3 of 7With the JavaScript loaded, what does pressing Post review send?
Choose an answer to see the explanation.
Question 4 of 7Why does this button call
addToCartinsidestartTransition?Read the code, then predictTSX"use client"; import { useTransition } from "react"; import { addToCart } from "./actions"; export default function AddButton({ id, name }: { id: string; name: string }) { const [isPending, startTransition] = useTransition(); function add() { startTransition(async () => { await addToCart(id); }); } return <button onClick={add}>{isPending ? `Adding ${name}…` : `Add ${name}`}</button>; }Choose an answer to see the explanation.
Question 5 of 7A visitor clicks Add Mint tea, then Add Green tea while the first is still running. When does Chrome send the second POST?
Choose an answer to see the explanation.
Question 6 of 7Where can
"use server"sit inside a function?Choose an answer to see the explanation.
Question 7 of 7The shop shows its Delete review form only to signed-in staff. Does
deleteReviewstill need to check who's asking?Choose an answer to see the explanation.
Key takeaways
"use server"at the top of a file makes every export a Server Action; at the top of an async function in a Server Component, it makes that one. Client Components import actions or get them as props.- A form calls an action with its fields, and works before the JavaScript loads: the browser posts the form and gets a page, or follows a
303when the action redirects. - With JavaScript, a call is a
fetch:POSTto the page, with anext-actionheader, answered with an RSC payload that can hold the updated page. - Outside forms, call actions in
startTransition, and showisPendingwhile they run. - Next.js sends a page's actions one at a time; the server doesn't queue them. Use actions for changes, not for reading.
- Every action is a public endpoint. Check who's asking and every argument inside it.
Remember the one-liner.
A Server Action is a function the browser calls and the server runs: one POST at a time, open to anyone.
Coming next: Forms. It validates what the visitor typed on the server, shows errors next to the fields, and keeps the text with useActionState.