Two directives, two doors
React Server Components is often described as having no API surface, and that's largely accurate: nearly the entire paradigm rests on two module-level directives, 'use client' and 'use server'. Their significance extends beyond React, though. They represent a way to express the client/server boundary inside the module system itself—a typed replacement for hand-written <script> tags and fetch() calls.
Seen this way, the concepts are simple:
'use client'is a typed<script>.'use server'is a typedfetch().
Together, they allow you to model a client/server application as a single program spanning two machines, without losing sight of the fact that the two sides don't share an execution context.
Server functions as network exports
Consider a conventional backend with API routes and a frontend that calls them. The backend might export route handlers that update a "like" state; the frontend calls those routes with fetch() and string URLs.
This works, but it is "stringly-typed": the connection between the frontend call site and the backend implementation is a convention, not a language construct. If you instead think of the frontend and backend as one program split between two computers, what you really want to express is simply a function call from one part of the program to another. Importing the backend's implementation directly into the frontend is meaningless—they can't share code. But what if the backend could annotate specific functions as exported over the network?
That is exactly what 'use server' does. Marking a file with the directive designates its exports as server functions. When client code imports from that file, it doesn't receive the implementations—it receives async functions that perform the corresponding HTTP requests under the hood. This is a specific flavor of RPC, which has existed for decades. What's new is that the connection is established syntactically, through the module system, rather than through a separate API layer.
With that syntactic connection in place, you get the usual benefits of first-class code relationships:
- Types can be enforced across the boundary, and serializability checks become possible.
- "Find All References" reveals which server functions are used on the client.
- Dead code analysis can flag or eliminate unused exports.
- Self-contained abstractions can encapsulate both sides of a feature, with no global API route namespace.
The directive turns the server/client connection from a matter of convention into something the module system understands. For the client, 'use server' opens a door to the server.
Client components as script references
The mirror problem is passing information from the backend to the frontend. The traditional approach renders HTML that includes a <script> tag; the browser loads that script, which attaches interactive logic. This has drawbacks: frontend logic tends to be global, and there's a disconnect between the server-rendered display and the client-side event handlers.
Component libraries solve this by making the frontend logic declarative. With a LikeButton component, the server's job is merely to pass initial props. On the client, those props drive rendering.
Even this flow, however, is stringly-typed. The act of passing information from the backend to the frontend still requires some convention—a script URL, a component name, a JSON blob. What if the backend could simply say which component it wants to mount and what data to give it?
The backend code would need some way to refer to a function that lives on the client. Again, a plain import is the most natural expression of that intent. When the backend imports from a file decorated with 'use client', it does not receive the component implementation. It receives a client reference—a description of a script to load and a function to call within it.
Under the hood, a component rendered as JSX can be serialized to JSON that includes that reference, and the reference ultimately generates the necessary <script> tags. The same reference also lets the server pre-run the component to generate initial HTML, if desired—that's optional, but it works from the same primitives.
From the server's perspective, importing from a 'use client' file gives it a handle on client-side code that it can render and pass data to. That handle is typecheckable and toolable, just like a normal import. For the server, 'use client' opens a door to the client.
Two worlds, a single program
The two directives are frequently misread as markers that designate where code "is"—as if 'use client' meant a file runs in the browser and 'use server' meant it runs on the server. That is not what they do.
'use client'exports client functions to the server. The server sees client references that can be rendered into<script>tags, with optional prerendering.'use server'exports server functions to the client. The client seesasyncfunctions that call the backend over HTTP.
The directives express the network gap within the module system. Neither side can execute the other's code directly—that's the reality of the execution boundary. What the directives enable is for one side to refer to the other side's code and pass information to it, all through ordinary import syntax.
This pattern generalizes beyond React and beyond JavaScript. It is, in essence, RPC at the module system level, paired with a mechanism for shipping code to the client. The server and client remain separated by time and space, unable to share an execution context. The directives don't remove that separation—they bridge it with language-level constructs, letting you compose abstractions that span both environments cleanly.



