From Overreacted · · 5m
How Imports Work in RSC
Dan Abramov rebuilds the frontend/backend split from first principles, and RSC turns out to be two small additions to the module system.
In brief
Dan Abramov rebuilds the frontend/backend split from first principles, and RSC turns out to be two small additions to the module system. RSC doesn't decide where your code runs. Poison pills stop code entering the wrong world, and 'use client' / 'use server' let one world point at the other without pulling it in…
One program, two computers
React Server Components let you write a client/server app as a single program. It does that by extending import and export with new meaning, so you control the frontend/backend split in code.
“RSC extends the module system (the import and export keywords) with novel semantics that let the developer control the frontend/backend split.”
Mostly not about RSC
Ninety percent of the argument is plain module mechanics and what breaks when you share code across the wire. RSC is the last ten percent that resolves the leftover tension.
“As usual, 90% of this article won't be about RSC.”
Modules are for humans
A computer just wants the whole program in memory. We split code into files so it fits in our heads, so we can hide implementation details, and so we can reuse other people's work.
“Modules let us break complex programs into parts that can fit into our brains.”
Bridging writing and running
A module system is the set of rules for splitting a program into files, deciding what can see what, and linking it all back into one runnable thing. In JS that surface is import and export.
Imports feel like paste
Import three files, call two functions, and the result behaves exactly like one flat file with everything typed out. That intuition carries you a long way.
“the import and export keywords are designed to work in a way that's reminiscent of copying and pasting”
C did it literally
C's #include predates JS imports by forty years and really does splice in the file contents. Two costs followed: name clashes between unrelated files, and the same file pasted in many times over.
Include guards by hand
C developers wrapped each header in a build-time "skip me if you already have me" guard. Modern module systems do that bookkeeping for you, automatically.
Every module runs once
If a.js and b.js both import c.js, c is still defined exactly once. Code inside a module executes at most once, no matter how many places import it.
“each JavaScript module is a singleton. Importing the same module twice will not execute its code twice.”
Why singletons matter
Bundles don't balloon from repetition, top-level variables can safely hold private state, and the mental model stays simple. Want something to run once? Put it at a module's top level.
A map under the hood
Loaders keep a map from filename to that module's exports. Node, webpack and Metro all have this cache somewhere, and a repeat import just reads from it.
Two sides, two caches
Write a backend and a frontend in JS and you have two programs on two machines. Import a shared file from each side and each side gets its own copy, running its own top-level code.
“Modules still act like singletons, but they are only singletons per environment.”
A build error is a gift
Put an fs call in shared code and the frontend build breaks, because fs isn't there. That failure is the system telling you early, before a confusing runtime crash.
“When we start reusing code between the two sides, we want to have some confidence that the code we're trying to reuse will actually work on both sides.”
No one right fix
You can move the fs call out, refactor the middle modules to stop depending on it, or stop importing them from the frontend entry. Which one is right depends on your intent, like resolving a merge conflict.
“It's not fun to resolve but the behavior you want is up to you (or an LLM) to decide.”
Secrets slip through silently
Now import a secret into that same shared file. Nothing fails, and the value lands in the browser bundle. Plenty of full-stack setups have no defense against exactly this.
“many fullstack apps don't employ any protection against a developer accidentally pulling in secrets into the frontend code like this!”
The server-only poison pill
Add an empty package that the frontend bundler is taught to reject. Import 'server-only' at the top of secrets.js and any bundle that reaches it dies at build time.
“By itself, that package will not contain any real code. It is a "poison pill".”
It spreads up the chain
You only mark the file that truly must stay put. The poison travels up every importer automatically, so the intermediate modules need no annotation at all.
“the important part about this solution is that it automatically propagates up the import chain”
The mirror twin
client-only does the same in reverse, failing the server build. Use it when a module reaches for prompt, the DOM, or anything else that has no meaning on the backend.
Pills don't place code
Neither marker moves anything anywhere. They only refuse entry to an environment that can't support the code, turning a runtime mystery into a build failure.
“They don't "put" the code "on the backend" or "on the frontend". The only thing these assertions do is prevent code from being pulled into an unsupported environment.”
Strings instead of references
The one weakness left: the server writes onClick="sayHello()" into an HTML string and hopes the frontend defines it. Importing sayHello wouldn't help, because that would drag it into the backend.
“This is kind of fragile.”
Two doors between worlds
'use client' makes an import return a reference React can revive as a script tag; 'use server' opens the same door back as an API call. Everywhere else, import stays import, and directories like frontend/ stop meaning anything.
“They let you refer to the other world.”
What it means
RSC doesn't decide where your code runs. Poison pills stop code entering the wrong world, and 'use client' / 'use server' let one world point at the other without pulling it in.

