my blog https://overreacted.io
53
fork

Configure Feed

Select the types of activity you want to include in your feed.

How Imports Work in RSC

+859
+1
public/how-imports-work-in-rsc/components.js
··· 1 + export * from "../react-for-two-computers/components";
+858
public/how-imports-work-in-rsc/index.md
··· 1 + --- 2 + title: How Imports Work in RSC 3 + date: '2025-06-05' 4 + spoiler: A layered module system. 5 + --- 6 + 7 + 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. 8 + 9 + I've [previously written](/what-does-use-client-do/) 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. 10 + 11 + 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. 12 + 13 + 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. 14 + 15 + Let's start with the fundamentals. 16 + 17 + --- 18 + 19 + ### What's a Module System? 20 + 21 + 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: 22 + 23 + - Modules let us break complex programs into parts that can fit into our brains. 24 + - 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. 25 + - Modules let us reuse code written by other humans (and by ourselves). 26 + 27 + **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.** 28 + 29 + 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. 30 + 31 + In JavaScript, the module system is exposed via `import` and `export` keywords. 32 + 33 + --- 34 + 35 + ### Imports Are Like Copy and Paste... 36 + 37 + Consider these two files, which we'll call `a.js` and `b.js`: 38 + 39 + ```js 40 + export function a() { 41 + return 2; 42 + } 43 + ``` 44 + 45 + ```js 46 + export function b() { 47 + return 2; 48 + } 49 + ``` 50 + 51 + By themselves, they don't do anything except defining some functions. 52 + 53 + Now consider this file called `index.js`: 54 + 55 + ```js {1-2} 56 + import { a } from './a.js'; 57 + import { b } from './b.js'; 58 + 59 + const result = a() + b(); // 4 60 + console.log(result); 61 + ``` 62 + 63 + Now, that's a module that ties them together into a single program! 64 + 65 + 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): 66 + 67 + ```js {1-7} 68 + function a() { 69 + return 2; 70 + } 71 + 72 + function b() { 73 + return 2; 74 + } 75 + 76 + const result = a() + b(); // 4 77 + console.log(result); 78 + ``` 79 + 80 + 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. 81 + 82 + --- 83 + 84 + ### ...Except They're Not 85 + 86 + Earlier I said imports are like copy and paste. That's not *exactly* true. To see why, it's intructive to take a trip down the memory lane to the `#include` directive in C. 87 + 88 + The `#include` directive, which predates the JavaScript `import` by about 40 years, behaved [quite literally like copy and paste](https://stackoverflow.com/a/5735389/458193)! For example, here's a C program: 89 + 90 + ```c {1-2} 91 + #include "a.h" 92 + #include "b.h" 93 + 94 + int main() { 95 + return a() + b(); 96 + } 97 + ``` 98 + 99 + 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: 100 + 101 + 1. One problem with `#include` is that [unrelated functions from different files would clash](https://softwareengineering.stackexchange.com/a/202156/3939) 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. 102 + 1. Another problem with `#include` is 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"](https://stackoverflow.com/a/12928949/458193) guard. Modern module systems, like `import`, do something similar automatically. 103 + 104 + Let's unpack that last point because it's important. 105 + 106 + --- 107 + 108 + ### JavaScript Modules Are Singletons 109 + 110 + Suppose we've added a new module called `c.js` that looks like this: 111 + 112 + ```js 113 + export function c() { 114 + return 2; 115 + } 116 + ``` 117 + 118 + 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: 119 + 120 + ```js {1} 121 + import { c } from './c.js'; 122 + 123 + export function a() { 124 + return c() * 2; 125 + } 126 + ``` 127 + 128 + ```js {1} 129 + import { c } from './c.js'; 130 + 131 + export function b() { 132 + return c() * 3; 133 + } 134 + ``` 135 + 136 + 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! 137 + 138 + 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:** 139 + 140 + ```js {1-3} 141 + function c() { 142 + return 2; 143 + } 144 + 145 + function a() { 146 + return c() * 2; 147 + } 148 + 149 + function b() { 150 + return c() * 3; 151 + } 152 + 153 + const result = a() + b(); // (2 * 2) + (2 * 3) = 10 154 + console.log(result); 155 + ``` 156 + 157 + 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. 158 + 159 + This is a crucial design choice that enables many advantages: 160 + 161 + - 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. 162 + - 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. 163 + - 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. 164 + 165 + 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](https://github.com/nodejs/node/blob/ed2c6965d2f901f3c786f9d24bcd57b2cd523611/lib/internal/modules/esm/loader.js#L114-L139), [webpack source](https://github.com/webpack/webpack/blob/19ca74127f7668aaf60d59f4af8fcaee7924541a/lib/javascript/JavascriptModulesPlugin.js#L1435-L1446), [Metro (RN) source](https://github.com/facebook/metro/blob/15fef8ebcf5ae0a13e7f0925a22d4211dde95e02/packages/metro-runtime/src/polyfills/require.js#L204-L209). 166 + 167 + 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. 168 + 169 + We've talked about *multiple modules,* but what about *multiple computers?* 170 + 171 + --- 172 + 173 + ### One Program, One Computer 174 + 175 + Most JavaScript programs are written for a single computer. 176 + 177 + That could be the browser, or a Node.js server, or some exotic JavaScript runtime. Still, I think it's safe to say **the majority of JS programs are written for a single machine to execute.** The program is loaded, the program runs, the program stops. 178 + 179 + The JavaScript module system, as described earlier, was designed to support exactly this most common use case. Here's one last recap of how it works: 180 + 181 + 1. There's some file that acts as an *entry point* into our program. In our earlier example, that was `index.js`. This is where the JavaScript engine starts. 182 + 2. This file may import other modules, like `a.js` or `b.js`, which themselves can import more modules. The JavaScript engine executes the code of those modules. It also stores the exports of each module in an in-memory cache for later. 183 + 3. If the JavaScript engine sees an `import` to a module it has *already loaded* (such as the second import to `c.js`), it's not going to run the module again. Modules are singletons! Instead, it will read that module's exports from an in-memory cache. 184 + 185 + Ultimately, it's convenient to think of the end result as being similar to copy-pasting the modules into one file, surgically renaming any clashing variables, and ensuring that the contents of each individual module is only ever included once: 186 + 187 + ```js 188 + /* c.js */ function c() { return 2; } 189 + /* a.js */ function a() { return c() * 2; } 190 + /* b.js */ function b() { return c() * 3; } 191 + 192 + const result = a() + b(); // (2 * 2) + (2 * 3) = 10 193 + console.log(result); 194 + ``` 195 + 196 + In that sense, when you `import` some code, you bring it *into* your program. 197 + 198 + But what if we want to write *both our backend and frontend* in JavaScript? (Or, alternatively, what if we realize that adding a [JS BFF can make our app better?](https://overreacted.io/jsx-over-the-wire/#backend-for-frontend)) 199 + 200 + --- 201 + 202 + ### Two Programs, Two Computers 203 + 204 + Traditionally, a frontend and a backend in JS means that we're working on two different programs that run on two different computers. In many cases, they might even be maintained by two different teams that barely talk to each other. 205 + 206 + Let's take a closer look at both of these programs. The *backend* is responsible for serving an HTML page (and potentially some APIs for more data-intensive apps). The *frontend* is responsible for pieces of the interactive logic on that HTML page. 207 + 208 + The backend code might live in `backend/index.js`: 209 + 210 + ```js 211 + function server() { 212 + return ( 213 + `<html> 214 + <body> 215 + <button onClick="sayHello()"> 216 + Press me 217 + </button> 218 + <script src="/frontend/index.js type="module"></script> 219 + </body> 220 + </html>` 221 + ); 222 + } 223 + ``` 224 + 225 + The frontend code might live in `frontend/index.js`: 226 + 227 + ```js 228 + function sayHello() { 229 + alert('Hi!'); 230 + } 231 + 232 + window['sayHello'] = sayHello; 233 + ``` 234 + 235 + Let's put them close to emphasize these are two different but related programs: 236 + 237 + <Server> 238 + 239 + ```js 240 + function server() { 241 + return ( 242 + `<html> 243 + <body> 244 + <button onClick="sayHello()"> 245 + Press me 246 + </button> 247 + <script src="/frontend/index.js type="module"></script> 248 + </body> 249 + </html>` 250 + ); 251 + } 252 + ``` 253 + 254 + </Server> 255 + 256 + <Client glued> 257 + 258 + ```js 259 + function sayHello() { 260 + alert('Hi!'); 261 + } 262 + 263 + window['sayHello'] = sayHello; 264 + ``` 265 + 266 + </Client> 267 + 268 + Now let's see what happens when we import something from either side. 269 + 270 + Suppose we import `a.js` and `b.js` from `backend/index.js`: 271 + 272 + <Server> 273 + 274 + ```js {1-2} 275 + import { a } from '../a.js'; 276 + import { b } from '../b.js'; 277 + 278 + function server() { 279 + return ( 280 + `<html> 281 + <body> 282 + <button onClick="sayHello()"> 283 + Press me 284 + </button> 285 + <script src="/frontend/index.js type="module"></script> 286 + </body> 287 + </html>` 288 + ); 289 + } 290 + ``` 291 + 292 + </Server> 293 + 294 + <Client glued> 295 + 296 + ```js 297 + function sayHello() { 298 + alert('Hi!'); 299 + } 300 + 301 + window['sayHello'] = sayHello; 302 + ``` 303 + 304 + </Client> 305 + 306 + Importing them *from* the backend code would bring them *into* the backend code: 307 + 308 + <Server> 309 + 310 + ```js {1-3} 311 + /* c.js */ function c() { return 2; } 312 + /* a.js */ function a() { return c() * 2; } 313 + /* b.js */ function b() { return c() * 3; } 314 + 315 + function server() { 316 + return ( 317 + `<html> 318 + <body> 319 + <button onClick="sayHello()"> 320 + Press me 321 + </button> 322 + <script src="/frontend/index.js type="module"></script> 323 + </body> 324 + </html>` 325 + ); 326 + } 327 + ``` 328 + 329 + </Server> 330 + 331 + <Client glued> 332 + 333 + ```js 334 + function sayHello() { 335 + alert('Hi!'); 336 + } 337 + 338 + window['sayHello'] = sayHello; 339 + ``` 340 + 341 + </Client> 342 + 343 + Now suppose that we *also* import them from `frontend/index.js`: 344 + 345 + <Server> 346 + 347 + ```js 348 + import { a } from '../a.js'; 349 + import { b } from '../b.js'; 350 + 351 + function server() { 352 + return ( 353 + `<html> 354 + <body> 355 + <button onClick="sayHello()"> 356 + Press me 357 + </button> 358 + <script src="/frontend/index.js type="module"></script> 359 + </body> 360 + </html>` 361 + ); 362 + } 363 + ``` 364 + 365 + </Server> 366 + 367 + <Client glued> 368 + 369 + ```js {1-2} 370 + import { a } from '../a.js'; 371 + import { b } from '../b.js'; 372 + 373 + function sayHello() { 374 + alert('Hi!'); 375 + } 376 + 377 + window['sayHello'] = sayHello; 378 + ``` 379 + 380 + </Client> 381 + 382 + Importing them *from* the frontend code brings them *into* the frontend code: 383 + 384 + <Server> 385 + 386 + ```js 387 + /* c.js */ function c() { return 2; } 388 + /* a.js */ function a() { return c() * 2; } 389 + /* b.js */ function b() { return c() * 3; } 390 + 391 + function server() { 392 + return ( 393 + `<html> 394 + <body> 395 + <button onClick="sayHello()"> 396 + Press me 397 + </button> 398 + <script src="/frontend/index.js type="module"></script> 399 + </body> 400 + </html>` 401 + ); 402 + } 403 + ``` 404 + 405 + </Server> 406 + 407 + <Client glued> 408 + 409 + ```js {1-3} 410 + /* c.js */ function c() { return 2; } 411 + /* a.js */ function a() { return c() * 2; } 412 + /* b.js */ function b() { return c() * 3; } 413 + 414 + function sayHello() { 415 + alert('Hi!'); 416 + } 417 + 418 + window['sayHello'] = sayHello; 419 + ``` 420 + 421 + </Client> 422 + 423 + Notice how the frontend and the backend don't share the module system! 424 + 425 + That's an important insight. Importing code *from* either side brings that code *into* that side--nothing more. The two sides have two *independent* module systems. Modules still act like singletons--but they are only *singletons per environment.* 426 + 427 + Although we are *reusing* the `a.js`, `b.js`, and `c.js` implementations between both sides, it would be more accurate to think that both the backend code and the frontend sides have "their own versions" of the `a.js`, `b.js`, and `c.js` modules. 428 + 429 + So far, there's nothing unusual about what I've described. It's how sharing code between the backend and the frontend has always worked in full-stack apps. However, as more of our code gets reused between the environments, we're risking accidentally reusing something that's *not meant* for the other side. 430 + 431 + How can we constrain and control code reuse? 432 + 433 + --- 434 + 435 + ### Build Failures Are Actually Good 436 + 437 + Suppose that somebody edits `c.js` to include some code that *only* makes sense on the backend. For example, imagine that we use `fs` to read a file on the server: 438 + 439 + ```js 440 + import { readFileSync } from 'fs'; 441 + 442 + export function c() { 443 + return Number(readFileSync('./number.txt', 'utf8')); 444 + } 445 + ``` 446 + 447 + This would not cause problems for the backend code: 448 + 449 + <Server> 450 + 451 + ```js 452 + /* fs.js */ function readFileSync() { /* ... */} 453 + /* c.js */ function c() { return Number(readFileSync('./number.txt', 'utf8')); } 454 + /* a.js */ function a() { return c() * 2; } 455 + /* b.js */ function b() { return c() * 3; } 456 + 457 + function server() { 458 + return ( 459 + `<html> 460 + <body> 461 + <button onClick="sayHello()"> 462 + Press me 463 + </button> 464 + <script src="/frontend/index.js type="module"></script> 465 + </body> 466 + </html>` 467 + ); 468 + } 469 + ``` 470 + 471 + </Server> 472 + 473 + However, it would fail the frontend build because `fs` does not exist there: 474 + 475 + <Client> 476 + 477 + ```js {1} 478 + import { readFileSync } from 'fs'; // 🔴 Build error: Cannot import 'fs' 479 + /* c.js */ function c() { return Number(readFileSync('./number.txt', 'utf8')); } 480 + /* a.js */ function a() { return c() * 2; } 481 + /* b.js */ function b() { return c() * 3; } 482 + 483 + function sayHello() { 484 + alert('Hi!'); 485 + } 486 + 487 + window['sayHello'] = sayHello; 488 + ``` 489 + 490 + </Client> 491 + 492 + And this is actually good! 493 + 494 + 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. 495 + 496 + If some APIs only make sense on one side (like `fs` only makes sense on the backend), we *want* the build to fail early so that we can decide how to fix our code: 497 + 498 + 1. We could choose to move the `fs` call somewhere other than `c.js`. 499 + 1. We could refactor `a.js` and `b.js` to not need `c.js`. 500 + 1. We could change `frontend/index.js` to not need `a.js` and `b.js`. 501 + 502 + It's important to note that **all of the above solutions are valid.** The solution you pick depends on what you're actually trying to do. There is no automated way to pick "the best" solution--if anything, this is similar to resolving a real Git conflict. It's not *fun* to resolve but the behavior you want *is up to you (or an LLM) to decide.* 503 + 504 + This is the price you pay for reusing code. The benefit is that it's easy to shift the logic around depending on which side needs it. The downside is that, when things blow up, you have to look at the build failure and *decide* which module needs a fix. 505 + 506 + In this case, we were lucky that importing something "on the wrong side" actually caused a build error. This let us immediately see the problem. But what if it didn't? 507 + 508 + --- 509 + 510 + ### Server-Only Code 511 + 512 + Suppose that instead, somebody edits `c.js` to import a server-side secret. 513 + 514 + ```js 515 + import { secret } from './secrets.js'; 516 + 517 + export function c() { 518 + return secret; 519 + } 520 + ``` 521 + 522 + This is much worse than the previous example! There would be no build failure, and the `secret` would become a part of both the backend *and* the frontend code: 523 + 524 + <Server> 525 + 526 + ```js {1-2} 527 + /* secrets.js */ const secret = 12345; 528 + /* c.js */ function c() { return secret; } 529 + /* a.js */ function a() { return c() * 2; } 530 + /* b.js */ function b() { return c() * 3; } 531 + 532 + function server() { 533 + return ( 534 + `<html> 535 + <body> 536 + <button onClick="sayHello()"> 537 + Press me 538 + </button> 539 + <script src="/frontend/index.js type="module"></script> 540 + </body> 541 + </html>` 542 + ); 543 + } 544 + ``` 545 + 546 + </Server> 547 + 548 + <Client glued> 549 + 550 + ```js {1-2} 551 + /* secrets.js */ const secret = 12345; 552 + /* c.js */ function c() { return secret; } 553 + /* a.js */ function a() { return c() * 2; } 554 + /* b.js */ function b() { return c() * 3; } 555 + 556 + function sayHello() { 557 + alert('Hi!'); 558 + } 559 + 560 + window['sayHello'] = sayHello; 561 + ``` 562 + 563 + </Client> 564 + 565 + This is a nightmare scenario, but many fullstack apps don't employ any protection against a developer accidentally pulling in secrets into the frontend code like this! 566 + 567 + How could we improve on that? 568 + 569 + Here's one idea. In the previous section, we've seen that using `fs` from the frontend code failed the frontend build, forcing us to actually fix the problem. 570 + 571 + That's *exactly* what we want to happen here, too! 572 + 573 + **Suppose that we create a special package, which we're going to call `server-only`, that serves as a *marker* for code that must never reach the frontend.** By itself, that package will not contain any real code. It is a "poison pill". We'll teach our frontend bundler to *fail the build* if this module gets into the frontend bundle. 574 + 575 + Assuming we've done that, we can now mark `secrets.js` as server-only: 576 + 577 + ```js 578 + import 'server-only'; 579 + 580 + export const secret = 12345; 581 + ``` 582 + 583 + With this change, pulling `secrets.js` into the bundle fails the frontend build. Concretely, both `a.js` and `b.js` will bring `c.js`, which will bring `secrets.js`, which will bring `server-only`--and that's the poison pill that fails the build: 584 + 585 + <Server> 586 + 587 + ```js {1-2} 588 + /* server-only */ /* (This does nothing on the backend.) */ 589 + /* secrets.js */ const secret = 12345; 590 + /* c.js */ function c() { return secret; } 591 + /* a.js */ function a() { return c() * 2; } 592 + /* b.js */ function b() { return c() * 3; } 593 + 594 + function server() { 595 + return ( 596 + `<html> 597 + <body> 598 + <button onClick="sayHello()"> 599 + Press me 600 + </button> 601 + <script src="/frontend/index.js type="module"></script> 602 + </body> 603 + </html>` 604 + ); 605 + } 606 + ``` 607 + 608 + </Server> 609 + 610 + <Client glued> 611 + 612 + ```js {1-2} 613 + /* server-only */ /* 🔴 (This fails the build on the frontend.) */ 614 + /* secrets.js */ const secret = 12345; 615 + /* c.js */ function c() { return secret; } 616 + /* a.js */ function a() { return c() * 2; } 617 + /* b.js */ function b() { return c() * 3; } 618 + 619 + function sayHello() { 620 + alert('Hi!'); 621 + } 622 + 623 + window['sayHello'] = sayHello; 624 + ``` 625 + 626 + </Client> 627 + 628 + Now we can control which code is not allowed to escape the backend! (As a concrete implementation example, here's the [related](https://github.com/vercel/next.js/blob/f684e973f1ddbbdc99cdda9a89070d6d228a1dd7/crates/next-custom-transforms/src/transforms/react_server_components.rs#L640) [logic](https://github.com/vercel/next.js/blob/f684e973f1ddbbdc99cdda9a89070d6d228a1dd7/crates/next-custom-transforms/src/transforms/react_server_components.rs#L772-L778) in the Next.js bundler.) 629 + 630 + Like with the `fs` import earlier, we'd have different options to fix it: 631 + 632 + 1. We could choose to move the `secrets.js` import somewhere other than `c.js`. 633 + 1. We could refactor `a.js` and `b.js` to not need `c.js`. 634 + 1. We could change `frontend/index.js` to not need `a.js` and `b.js`. 635 + 636 + But the important part about this solution is that it *automatically propagates up the import chain*. You don't need to mark individual files like `a.js`, `b.js`, and `c.js` as server-only unless there's some specific reason *local to them* that must prevent their inclusion. It's enough to mark files that should *definitely* not be included (like `secrets.js`), and then rely on the "poison pill" propagating up the import chain. 637 + 638 + --- 639 + 640 + ### Client-Only Code 641 + 642 + Similarly to the `server-only` "poison pill", we can create a mirror twin `client-only` "poison pill" that fails the server-side build. (If you don't bundle the server, you could instead run this check separately similar to running TypeScript.) 643 + 644 + Suppose that we used a browser-specific API in `c.js`. This may be a decent reason for us to decide that it's *never valid* to pull it into the backend code: 645 + 646 + ```js {1} 647 + import 'client-only'; 648 + 649 + export function c() { 650 + return Number(prompt('How old are you?')); 651 + } 652 + ``` 653 + 654 + This is not as critical, but it helps discover mistakes more quickly. Our goal is to turn confusing *runtime* errors that stem from importing code that wasn't meant for the other side--like DOM logic--into *build* errors that force us to fix it: 655 + 656 + <Server> 657 + 658 + ```js {1-2} 659 + /* client-only */ /* 🔴 (This fails the build on the backend.) */ 660 + /* c.js */ function c() { return Number(prompt('How old are you?')); } 661 + /* a.js */ function a() { return c() * 2; } 662 + /* b.js */ function b() { return c() * 3; } 663 + 664 + function server() { 665 + return ( 666 + `<html> 667 + <body> 668 + <button onClick="sayHello()"> 669 + Press me 670 + </button> 671 + <script src="/frontend/index.js type="module"></script> 672 + </body> 673 + </html>` 674 + ); 675 + } 676 + ``` 677 + 678 + </Server> 679 + 680 + <Client glued> 681 + 682 + ```js {1-2} 683 + /* client-only */ /* (This does nothing on the frontend.) */ 684 + /* c.js */ function c() { return Number(prompt('How old are you?')); } 685 + /* a.js */ function a() { return c() * 2; } 686 + /* b.js */ function b() { return c() * 3; } 687 + 688 + function sayHello() { 689 + alert('Hi!'); 690 + } 691 + 692 + window['sayHello'] = sayHello; 693 + ``` 694 + 695 + </Client> 696 + 697 + Again, this would present us with a choice: 698 + 699 + 1. We could refactor `c.js` to work on the backend (and remove the poison pill). 700 + 1. We could refactor `a.js` and `b.js` to not need `c.js`. 701 + 1. We could change `backend/index.js` to not need `a.js` and `b.js`. 702 + 703 + We could further envision a more granular version of `client-only` and `server-only` that applies to individual package imports. For example, the React package could declare APIs like `useState` and `useEffect` to be `client-only` so that pulling them into the backend code immediately fails the build. (Hint: React actually *does* do that via the [`package.json` Conditional Exports](https://nodejs.org/api/packages.html#conditional-exports) mechanism.) 704 + 705 + I suspect you're starting to see a theme here. As we move to share and reuse more code between the backend and frontend codebases--and indeed, as these two codebases blend into one--these build-time assertions give us a peace of mind. 706 + 707 + Not every module *needs* to be exclusive to some side. In fact, most modules don't care because they aren't the *source* of incompatibilities. For example, `a.js` and `b.js` don't prescribe that they must only exist on one side because they *don't know* the implementation details of `c.js.` But if some module *does* care to be exclusive, it can now express this "locally" with `server-only` or `client-only`. The declared incompatibility then transitively "infects" every importing module. 708 + 709 + It is also crucial to understand that the `server-only` and `client-only` "poison pills" do not *control* where the code goes. 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. They're poison pills *only.* 710 + 711 + By this point, we've almost invented RSC. 712 + 713 + There's just one last detail left. 714 + 715 + --- 716 + 717 + ### One Program, Two Computers 718 + 719 + Let's have one more look at our backend and our frontend as separate programs: 720 + 721 + <Server> 722 + 723 + ```js 724 + function server() { 725 + return ( 726 + `<html> 727 + <body> 728 + <button onClick="sayHello()"> 729 + Press me 730 + </button> 731 + <script src="/frontend/index.js type="module"></script> 732 + </body> 733 + </html>` 734 + ); 735 + } 736 + ``` 737 + 738 + </Server> 739 + 740 + <Client glued> 741 + 742 + ```js 743 + function sayHello() { 744 + alert('Hi!'); 745 + } 746 + 747 + window['sayHello'] = sayHello; 748 + ``` 749 + 750 + </Client> 751 + 752 + By now, we have a good mental model for how these programs can share code: 753 + 754 + - Importing code *from* either side always bring it *into* that side. 755 + - The two module systems remain completely independent. If you import some shared code from *both* sides, it will be independently brought *into* both sides. 756 + - By default, we assume that any code is reusable. But we provide `server-only` and `client-only` poison pills that should be used in modules that *should never get brought into* a particular side due to some code *directly inside of* those modules. This doesn't change how or where the code *runs,* but it gives us early build errors. 757 + 758 + Honestly, we could stop here, and we'd have a compelling setup for full-stack development that provides safer code reuse than many popular setups. 759 + 760 + However, there's one remaining weakness in our approach. Currently, the backend code and the frontend code rely on *convention* to stay in sync. The backend wants to *refer* to the `sayHello` function from the frontend, but there's no way to do it syntactically so it has to resort to *assuming* that it will exist on the other side: 761 + 762 + <Server> 763 + 764 + ```js {5-8} 765 + function server() { 766 + return ( 767 + `<html> 768 + <body> 769 + <button onClick="sayHello()"> 770 + Press me 771 + </button> 772 + <script src="/frontend/index.js type="module"></script> 773 + </body> 774 + </html>` 775 + ); 776 + } 777 + ``` 778 + 779 + </Server> 780 + 781 + <Client glued> 782 + 783 + ```js {5} 784 + function sayHello() { 785 + alert('Hi!'); 786 + } 787 + 788 + window['sayHello'] = sayHello; 789 + ``` 790 + 791 + </Client> 792 + 793 + This is kind of fragile. 794 + 795 + Of course, the backend couldn't *just import* `sayHello`, for--as an observant reader might already realize--that would just bring it *into* the backend code. 796 + 797 + It would be nice if there was some way for the backend code to *refer* to `sayHello` without bringing it *into* the backend. Luckily, that is what [`'use client'` does](/what-does-use-client-do/): 798 + 799 + <Server> 800 + 801 + ```js {1} 802 + import { sayHello } from '../frontend/index.js'; 803 + 804 + function Server() { 805 + return ( 806 + <html> 807 + <body> 808 + <button onClick={sayHello}> 809 + Press me 810 + </button> 811 + </body> 812 + </html> 813 + ); 814 + } 815 + ``` 816 + 817 + </Server> 818 + 819 + <Client glued> 820 + 821 + ```js {1,3} 822 + 'use client'; 823 + 824 + export function sayHello() { 825 + alert('Hi.'); 826 + } 827 + ``` 828 + 829 + </Client> 830 + 831 + That's the "remaining 10%" that RSC adds. 832 + 833 + In RSC, imports on both sides normally *work like regular imports*--but `'use client'` changes this behavior to "opening a door" to the frontend environment. When you add `'use client'`, you're saying: "If you import me from the backend world, don't actually bring my code *into* the backend--instead, provide a reference that React can turn eventually into a `<script>` tag and revive on the frontend. 834 + 835 + Likewise, [`'use server'`](/what-does-use-client-do/#use-server) lets a piece of the frontend code "open the door" to the backend and *refer* to a backend module *without* bringing it *into* the frontend world. 836 + 837 + **The directives aren't for specifying "where the code runs" module by module. You shouldn't put `'use client'` in all frontend modules or `'use server'` into all backend modules--that's pointless!** All they do is let you create "doors" between the two module systems. They let you *refer to* the other world. 838 + 839 + If you want to pass data from the backend to the frontend (as a `<script>` tag), you need to `'use client'`. If you need to pass data from the frontend to the backend (as an API call), you need to `'use server'`. Otherwise, you don't need either directive--you just use `import` as usual and stay in the current world. 840 + 841 + --- 842 + 843 + ### In Conclusion 844 + 845 + RSC does not shy away from the fact that both the backend and the frontend each have its own module system. It works exactly like traditional JavaScript codebases that reuse some code between the frontend and the backend, where reused code effectively exists on both sides. What RSC adds on top are just two mechanisms: 846 + 847 + - The `import 'client-only'` and `import 'server-only'` poison pills that let some individual modules declare they *must not be brought* into the other world. 848 + - The `'use client'` and `'use server'` directives that let you *refer* to the modules from the other world and pass data to them without *bringing them in*. 849 + 850 + With these two mechanisms, you can see an RSC application as a single program spanning two computers--with two independent module systems, two poison pills, and two doors to pass information between those module systems. 851 + 852 + As this "layered" approach settles in your muscle memory, you'll realize that the `frontend/` and `backend/` directories become unnecessary and downright misleading because the information is already contained in the modules. But it's contained *locally* so the boundaries automatically shift as you evolve your code. 853 + 854 + The poison pills ensure that nothing is brought into a wrong world, the directives let you pass information between the worlds, and regular imports work as usual. 855 + 856 + Now all you have to do is to fix the build errors. 857 + 858 + I heard LLMs are getting quite good at that.