How Imports Work in RSC
A layered module system.
How Imports Work in RSC
June 5, 2025
React Server Components (RSC) is a programming paradigm that lets you express a client/server application as a single program spanning over two environments. Concretely, RSC extends the module system (the import and export keywords) with novel semantics that let the developer control the frontend/backend split.
I’ve previously written about the 'use client' and 'use server' directives which mark the “split points” between the two environments. In this post, I’d like to focus on how these directives interact with the import and export keywords.
This post is a deep dive for anyone who’d like to build an accurate mental model of RSC, as well as for folks who are interested in module systems in general. You might find the RSC approach both surprising and simpler than you might think.
As usual, 90% of this article won’t be about RSC. It’s about how imports work in general, and what happens when we try to share code between the backend and the frontend. My aim is to show how RSC provides a natural solution to the last 10% of tensions that arise when we write code spanning both sides of the wire.
Let’s start with the fundamentals.
What’s a Module System?
When a computer executes a program, it doesn’t need “modules”. The computer needs the program’s code and data to be fully loaded in memory before it can run and process them. It’s actually us humans who want to split code into modules:
- Modules let us break complex programs into parts that can fit into our brains.
- Modules let us constrain which lines of code are meant to be visible (or exported) to other parts of the code, and which should remain an implementation detail.
- Modules let us reuse code written by other humans (and by ourselves).
We want to author our programs as split into parts—but executing a program involves “unrolling” those parts in memory. The job of a module system is to bridge the gap between how humans write code and how computers execute it.
Concretely, a module system is a set of rules that specify how a program can be split into files, how the developer controls which parts can “see” which other parts, and how those parts get linked into a single program that can be loaded in memory.
In JavaScript, the module system is exposed via import and export keywords.
Imports Are Like Copy and Paste…
Consider these two files, which we’ll call a.js and b.js:
export function a() {
return 2;
export function b() {
return 2;
By themselves, they don’t do anything except defining some functions.
Now consider this file called index.js:
import { a } from './a.js';
import { b } from './b.js';
const result = a() + b(); // 4
console.log(result);
Now, that’s a module that ties them together into a single program!
The rules of the JavaScript module system are complex. There are many intricacies to how it works. But there’s a simple intuition we can use. The JavaScript module system is designed to ensure that by the time the program above runs, it should behave identically to this single-file program (which doesn’t use modules at all):
function a() {
return 2;
function b() {
return 2;
const result = a() + b(); // 4
console.log(result);
In other words, the import and export keywords are designed to work in a way that’s reminiscent of copying and pasting—because ultimately, in the end, the program does need to be “unrolled” in the process’s memory by the JS engine.
…Except They’re Not
Earlier I said imports are like copy and paste. That’s not exactly true. To see why, it’s instructive to take a trip down the memory lane to the #include directive in C.
The #include directive, which predates the JavaScript import by about 40 years, behaved quite literally like copy and paste! For example, here’s a C program:
#include "a.h"
#include "b.h"
int main() {
return a() + b();
In C, the #include directive would literally embed the entire contents of a.h and b.h into the file above. This behavior is simple, but it has two big downsides:
- One problem with
#includeis that unrelated functions from different files would clash if their names were the same. That’s something we take for granted with modern module systems, where all identifiers are local to the file they’re in.
- Another problem with
#includeis that the same file could get “included” from several places—and thus get repeated in the output program many times! To work around this, the best practice was to surround the contents of each file you want to be “includable” with a build-time “skip including me if you already included me” guard. Modern module systems, likeimport, do something similar automatically.
Let’s unpack that last point because it’s important.
JavaScript Modules Are Singletons
Suppose we’ve added a new module called c.js that looks like this:
export function c() {
return 2;
Now suppose that we’ve rewritten both a.js and b.js so that each of them imports the c function from the c.js file and does something with it:
import { c } from './c.js';
export function a() {
return c() * 2;
import { c } from './c.js';
export function b() {
return c() * 3;
If import was literally copy-and-paste (like #include), we’d end up with two copies of the c function in our program. But thankfully, that’s not what happens!
The JavaScript module system ensures that the code above, along with index.js file from earlier, is equivalent in its semantics to the single-file program below. Notice how the c function is defined once despite having been imported twice:
function c() {
return 2;
function a() {
return c() * 2;
function b() {
return c() * 3;
const result = a() + b(); // (2 * 2) + (2 * 3) = 10
console.log(result);
In other words, modern module systems, such as the JavaScript module system, guarantee that the code inside each individual module executes at most once, no matter how many times and from how many places that module gets imported.
This is a crucial design choice that enables many advantages:
- When the code is turned into a single program (whether as an executable, as a bundle, or in-memory), the output size does not “explode” from repetition.
- Each module can keep some “private state” in top-level variables and be sure that it’s retained (and not recreated) no matter how many times it got imported.
- The mental model is dramatically simpler because each module is a “singleton”. If you want some code to only execute once, write it at the top level of its module.
Under the hood, module systems usually do this by holding a Map that keeps track of which modules (keyed by their filename) have already been loaded, and what their exported values are. Any JS import implementation will have this logic somewhere, for example: Node.js source, webpack source, Metro (RN) source.
Let’s repeat that: each JavaScript module is a singleton. Importing the same module twice will not execute its code twice. Every module runs at most once.
We’ve talked about multiple modules, but what about multiple computers?
[...]