
Next.js Directives
If you've spent any time in the App Router, you've typed 'use client' at the top of a file without thinking too hard about what that string literal actually does. It looks like a comment. It behaves like a compiler switch. That gap between how directives look and what they do is exactly where a lot of confusion creeps in — developers treat them as documentation-style hints, then get surprised when the build fails because a directive was in the wrong place, or a function it covered wasn't async.
Directives are one of the few places in Next.js where a single line of code changes how the compiler treats everything around it. Understanding what they are, how the five of them relate to each other, and where each one is legal to place saves you from an entire category of "why won't this build" debugging sessions.
What a directive actually is
A directive is a string literal — just 'use client' or 'use cache' sitting on its own line — that Next.js and React read as an instruction rather than as inert code. Nothing about the syntax marks it as special; there's no keyword, no import, no decorator. The compiler recognizes specific string literals in specific positions and transforms the surrounding code based on which one it finds.
This is a deliberate design choice, not an accident of history. Directives need to survive being read by a bundler before any of your code executes, and a plain string literal is the cheapest possible thing to detect statically. It's the same trick 'use strict' used in vanilla JavaScript for years — a string that means nothing to a naive interpreter but means everything to a tool that knows to look for it.
Right now there are three directives, with two of them having named variants:
| Directive | Defined by | Effect |
|---|---|---|
'use client' | React | Creates a client entry point from a Server Component |
'use server' | React | Exposes a function as a Server Function callable from the client |
'use cache' | Next.js | Caches a function's or component's output based on its inputs |
'use cache' additionally has 'use cache: private' (for functions that read request-time data like cookies) and 'use cache: remote' (for output stored in a shared, persistent cache handler rather than an in-memory one). Those two get their own dedicated articles in this blog — here I'm covering how the family fits together.
The mental model: two directives about the network boundary, one about caching
It helps to stop thinking of these five as one flat list and instead split them by what they're actually solving:
'use client' and 'use server' are about where code executes. They exist because the App Router's default is that everything is a Server Component unless told otherwise, and at some point your app needs interactivity (client state, event handlers, effects) or needs to let the client trigger server-only work (a database write, a call to a secret-holding API). These two directives mark the boundary crossings in each direction.
'use cache' (and its variants) is about how long output is kept around. It has nothing to do with client/server boundaries — a cached function can be called from anywhere code already runs. It's purely a performance decision: don't redo this work if the inputs haven't changed.
Conflating the two categories is where I've seen people get tripped up. 'use client' doesn't cache anything, and 'use cache' doesn't move code to the client. They compose with each other but solve unrelated problems.
Where each directive is legal to place
This is the part the docs are strict about, and it's not symmetrical across the three.
'use client' must be the first line of the file, before any imports. Not "near the top" — the literal first line. It applies to the entire module, because the whole file gets bundled and shipped to the browser as a unit. There's no such thing as an inline or function-scoped 'use client'; the directive marks a file-wide boundary because that's the granularity at which bundling actually happens.
// app/components/counter.tsx
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
'use server' and 'use cache' are more flexible — they can go at the top of a file (applying to every export) or at the top of an individual function (applying to just that one).
// app/data.ts — file-level: every export below is cached
"use cache";
export async function getUser(id: string) {
return { id };
}
// app/user.tsx — function-level: only this function is cached
export async function User() {
"use cache";
return <p>User</p>;
}
The rule of thumb I use: if you're covering one function, put the directive inside that function. If you're covering most or all of a file's exports, put it at the top and skip repeating it everywhere. Mixing styles in the same file — a file-level directive plus a redundant function-level one — doesn't break anything, but it does make it harder to tell at a glance what's actually covered.
One consequence that catches people off guard: with a file-level 'use cache' on a page or layout, every exported function in that file — including generateMetadata and generateStaticParams if you've defined them there — has to be async. The directive doesn't selectively apply; it's file-wide once you put it at the top.
The async requirement isn't optional
Both 'use server' and 'use cache' require every function they cover to be declared async, even if the function body doesn't actually await anything. This trips people up constantly, because a function like this looks fine at a glance:
"use cache";
export function getStaticValue() {
return { value: 42 };
}
It isn't. Next.js needs the function to return a Promise so it can slot into the caching and streaming machinery the same way every other cached or server-invoked function does. Add async even to trivial functions, and the compiler stops complaining.
Server and Client aren't interchangeable — but Server Functions can cross
'use client' marks the boundary where props have to become serializable — plain objects, arrays, strings, numbers, and a specific list of other types React knows how to send across the wire. An ordinary function, like a closure capturing local variables, cannot be passed as a prop into a Client Component. It simply doesn't survive serialization.
Server Functions are the one exception. A function marked with 'use server' can be passed into a Client Component as a prop, because what actually crosses the boundary isn't the function's code — it's a reference. When the Client Component calls it, that call gets serialized, sent to the server, executed there, and the result serialized back. The client never sees the implementation; it just holds a callable pointer to it.
This is why you'll see 'use server' functions passed as action props to forms, or invoked directly from onClick handlers in Client Components, in a way that looks like it shouldn't work if you're thinking about the client/server split too literally. It works because the directive changes what "passing a function" means for that specific function.
The reverse restriction matters too: you can't declare 'use server' or 'use cache' inside a file that already has 'use client' at the top. Server Functions and cached functions belong in server modules. If you need to call one from a Client Component, define it in a separate server file and import it — the import itself becomes the reference-passing mechanism.
Placement decides the size of what you're caching
For 'use cache' specifically, where you put the directive determines the granularity of the cache entry, which is easy to get wrong in a way that doesn't show up as an error — it just quietly caches more or less than you intended.
Put 'use cache' at the top of a page file, and Next.js caches the page's rendered output along with every component it imports and renders. Put the same directive inside one data-fetching function instead, and only that function's return value gets cached — the rest of the page re-renders normally around it.
// Caches just the expensive part, not the whole page
export async function getExpensiveReport() {
"use cache";
const data = await computeSomethingSlow();
return data;
}
If one function is doing all the expensive work on an otherwise cheap page, cache that function directly rather than reaching for a page-level directive. Page-level caching is convenient, but it's a blunter instrument — it caches everything downstream, including cheap, frequently-changing pieces that didn't need it.
The 'use cache: remote' variant changes where that cached entry lives — in a remote cache handler you configure, rather than an ephemeral in-memory store — which matters for multi-instance deployments where you need cache hits to be shared across servers. It doesn't change the placement rules above; it changes storage and, worth remembering, larger cached payloads through a remote handler cost more in both storage and network transfer than they would sitting in local memory.
Debugging still points at your source
One detail the docs call out that's genuinely reassuring: directives are applied at compile time, in both development and production, but Next.js's error overlay, dev indicators, and stack traces all point back to your original source file and line — not to whatever transformed output the compiler generated internally. If a cached function throws, you're debugging your code, not generated boilerplate.
Key Takeaways
| Directive | Placement | Requires async | Crosses client/server boundary |
|---|---|---|---|
'use client' | First line of file only | No | Yes — defines the client entry point |
'use server' | File-level or function-level | Yes | Yes — as a callable reference |
'use cache' | File-level or function-level | Yes | No — pure performance optimization |
'use cache: private' | Same as 'use cache' | Yes | No — but can read request-time data |
'use cache: remote' | Same as 'use cache' | Yes | No — output stored in a shared remote handler |
The five directives split cleanly into two jobs: 'use client' and 'use server' decide where code runs and how it crosses the network boundary, while the 'use cache' family decides how long output sticks around before it's recomputed. Once you stop lumping all five into one mental bucket labeled "directives," the placement rules and async requirements stop feeling arbitrary — they're just the mechanical consequences of what each one is actually doing under the hood.


