my blog https://overreacted.io

What Does use client Do?

+587
+1
public/what-does-use-client-do/components.js
··· 1 + export * from "../react-for-two-computers/components";
+586
public/what-does-use-client-do/index.md
··· 1 + --- 2 + title: What Does "use client" Do? 3 + date: '2025-04-25' 4 + spoiler: Two worlds, two doors. 5 + --- 6 + 7 + React Server Components (in?)famously has no API surface. It's an entire programming paradigm largely stemming from two directives: 8 + 9 + - `'use client'` 10 + - `'use server'` 11 + 12 + I'd like to make a bold claim that their invention belongs in the same category as structured programming (`if` / `while`), first-class functions, [and `async`/`await`.](https://tirania.org/blog/archive/2013/Aug-15.html) In other words, I expect them to survive past React and to become common sense. 13 + 14 + The server *needs* to send code to the client (by sending a `<script>`). The client *needs* to talk back to the server (by doing a `fetch`). The `'use client'` and `'use server'` directives abstract over those, offering a first-class, typed, and statically analyzable way to pass control to a piece of your codebase on another computer: 15 + 16 + - **`'use client'` is a typed `<script>`.** 17 + - **`'use server'` is a typed `fetch()`.** 18 + 19 + Together, these directives let you express the client/server boundary *within* the module system. They let you model a client/server application *as a single program spanning the two machines* without losing sight of the reality of the network and serialization gap. That, in turn, allows [seamless composition across the network.](/impossible-components/) 20 + 21 + Even if you never plan to use React Server Components, I think you should learn about these directives and how they work anyway. They're not even about React. 22 + 23 + They are about the module system. 24 + 25 + --- 26 + 27 + ### `'use server'` 28 + 29 + First, let's look at `'use server'`. 30 + 31 + Suppose you're writing a backend server that has some API routes: 32 + 33 + <Server> 34 + 35 + ```js {15-25} 36 + async function likePost(postId) { 37 + const userId = getCurrentUser(); 38 + await db.likes.create({ postId, userId }); 39 + const count = await db.likes.count({ where: { postId } }); 40 + return { likes: count }; 41 + } 42 + 43 + async function unlikePost(postId) { 44 + const userId = getCurrentUser(); 45 + await db.likes.destroy({ where: { postId, userId } }); 46 + const count = await db.likes.count({ where: { postId } }); 47 + return { likes: count }; 48 + } 49 + 50 + app.post('/api/like', async (req, res) => { 51 + const { postId } = req.body; 52 + const json = await likePost(postId); 53 + res.json(json); 54 + }); 55 + 56 + app.post('/api/unlike', async (req, res) => { 57 + const { postId } = req.body; 58 + const json = await unlikePost(postId); 59 + res.json(json); 60 + }); 61 + ``` 62 + 63 + </Server> 64 + 65 + Then you have some frontend code that calls these API routes: 66 + 67 + <Client> 68 + 69 + ```js {4-9,13-18} 70 + document.getElementById('likeButton').onclick = async function() { 71 + const postId = this.dataset.postId; 72 + if (this.classList.contains('liked')) { 73 + const response = await fetch('/api/unlike', { 74 + method: 'POST', 75 + headers: { 'Content-Type': 'application/json' }, 76 + body: JSON.stringify({ postId }) 77 + }); 78 + const { likes } = await response.json(); 79 + this.classList.remove('liked'); 80 + this.textContent = likes + ' Likes'; 81 + } else { 82 + const response = await fetch('/api/like', { 83 + method: 'POST', 84 + headers: { 'Content-Type': 'application/json' }, 85 + body: JSON.stringify({ postId, userId }) 86 + }); 87 + const { likes } = await response.json(); 88 + this.classList.add('liked'); 89 + this.textContent = likes + ' Likes'; 90 + } 91 + }); 92 + ``` 93 + 94 + </Client> 95 + 96 + (For simplicity, this example doesn't try to handle race conditions and errors.) 97 + 98 + This code is all dandy and fine, but it is ["stringly-typed"](https://www.hanselman.com/blog/stringly-typed-vs-strongly-typed). What we're trying to do is to *call a function on another computer*. However, since the backend and the frontend are two separate programs, we have no way to express that other than a `fetch`. 99 + 100 + Now imagine we thought about the frontend and the backend as *a single program split between two machines*. How would we express the fact that a piece of code wants to call another piece of code? What is the most direct way to express that? 101 + 102 + If we set aside all of our preconceived notions about how the backend and the frontend "should" be built for a moment, we can remember that we're *really* trying to say is that we want to *call* `likePost` and `unlikePost` from our frontend code: 103 + 104 + <Client> 105 + 106 + ```js {1,6,10} 107 + import { likePost, unlikePost } from './backend'; // This doesn't work :( 108 + 109 + document.getElementById('likeButton').onclick = async function() { 110 + const postId = this.dataset.postId; 111 + if (this.classList.contains('liked')) { 112 + const { likes } = await likePost(postId); 113 + this.classList.remove('liked'); 114 + this.textContent = likes + ' Likes'; 115 + } else { 116 + const { likes } = await unlikePost(postId); 117 + this.classList.add('liked'); 118 + this.textContent = likes + ' Likes'; 119 + } 120 + }; 121 + ``` 122 + 123 + </Client> 124 + 125 + The problem is, of course, `likePost` and `unlikePost` cannot actually execute on the frontend. We can't literally import their implementations *into* the frontend. Importing the backend directly from the frontend is by definition *meaningless.* 126 + 127 + However, suppose that there was a way to annotate the `likePost` and `unlikePost` functions as being *exported from the server* at the module level: 128 + 129 + <Server> 130 + 131 + ```js {1,3,10} 132 + 'use server'; // Mark all exports as "callable" from the frontend 133 + 134 + export async function likePost(postId) { 135 + const userId = getCurrentUser(); 136 + await db.likes.create({ postId, userId }); 137 + const count = await db.likes.count({ where: { postId } }); 138 + return { likes: count }; 139 + } 140 + 141 + export async function unlikePost(postId) { 142 + const userId = getCurrentUser(); 143 + await db.likes.destroy({ where: { postId, userId } }); 144 + const count = await db.likes.count({ where: { postId } }); 145 + return { likes: count }; 146 + } 147 + ``` 148 + 149 + </Server> 150 + 151 + We could then automate setting up the HTTP endpoints behind the scenes. And now that we have an opt-in syntax for exporting functions over the network, we could assign *meaning* to importing them from the frontend code--`import`ing them could simply give us `async` functions that perform those HTTP calls: 152 + 153 + <Client> 154 + 155 + ```js {1,6,10} 156 + import { likePost, unlikePost } from './backend'; 157 + 158 + document.getElementById('likeButton').onclick = async function() { 159 + const postId = this.dataset.postId; 160 + if (this.classList.contains('liked')) { 161 + const { likes } = await likePost(postId); // Call over HTTP 162 + this.classList.remove('liked'); 163 + this.textContent = likes + ' Likes'; 164 + } else { 165 + const { likes } = await unlikePost(postId); // Call over HTTP 166 + this.classList.add('liked'); 167 + this.textContent = likes + ' Likes'; 168 + } 169 + }; 170 + ``` 171 + 172 + </Client> 173 + 174 + That's precisely what the `'use server'` directive is. 175 + 176 + This is not a new idea--[RPC has been around for decades.](https://en.wikipedia.org/wiki/Remote_procedure_call) This is just a specific flavor of RPC for client-server applications where the server code can designate some functions as "server exports" (`'use server'`). Importing `likePost` from the server code works the same as a normal `import`, but importing `likePost` *from the client code* gives you an `async` function that performs the HTTP call. 177 + 178 + Have another look at this pair of files: 179 + 180 + <Server> 181 + 182 + ```js {1,3,10} 183 + 'use server'; 184 + 185 + export async function likePost(postId) { 186 + const userId = getCurrentUser(); 187 + await db.likes.create({ postId, userId }); 188 + const count = await db.likes.count({ where: { postId } }); 189 + return { likes: count }; 190 + } 191 + 192 + export async function unlikePost(postId) { 193 + const userId = getCurrentUser(); 194 + await db.likes.destroy({ where: { postId, userId } }); 195 + const count = await db.likes.count({ where: { postId } }); 196 + return { likes: count }; 197 + } 198 + ``` 199 + 200 + </Server> 201 + 202 + <Client glued> 203 + 204 + ```js {1,6,10} 205 + import { likePost, unlikePost } from './backend'; 206 + 207 + document.getElementById('likeButton').onclick = async function() { 208 + const postId = this.dataset.postId; 209 + if (this.classList.contains('liked')) { 210 + const { likes } = await likePost(postId); 211 + this.classList.remove('liked'); 212 + this.textContent = likes + ' Likes'; 213 + } else { 214 + const { likes } = await unlikePost(postId); 215 + this.classList.add('liked'); 216 + this.textContent = likes + ' Likes'; 217 + } 218 + }; 219 + ``` 220 + 221 + </Client> 222 + 223 + You may have objections--yes, it doesn't allow multiple consumers of the API (unless they're within the same codebase); yes, it requires some thought as to versioning and deployment; yes, it is more implicit than writing a `fetch`. 224 + 225 + But if you adopt the view that the backend and a frontend are *a single program split across two computers,* you can't really "unsee" it. There is now a direct and visceral connection between the two modules. You can add types to narrow down their contract (and enforce that their types are serializable). You can use "Find All References" to see which functions from the server are used on the client. Unused endpoints can be automatically flagged and/or eliminated with dead code analysis. 226 + 227 + Most importantly, you can now create self-contained abstractions that fully encapsulate both sides--a "frontend" attached to its corresponding "backend" piece. You don't need to worry about an explosion of API routes--the server/client split can be as modular as your abstractions. There is no global naming scheme; you organize the code using `export` and `import`, wherever you need them. 228 + 229 + The `'use server'` directive makes the connection between the server and the client *syntactic*. It is no longer a matter of convention--it's *in* your module system. 230 + 231 + It opens a *door* to the server. 232 + 233 + --- 234 + 235 + ### `'use client'` 236 + 237 + Now suppose that you want to pass some information from the backend to the frontend code. For example, you might render some HTML with a `<script>`: 238 + 239 + <Server> 240 + 241 + ```js {8-14} 242 + app.get('/posts/:postId', async (req, res) => { 243 + const { postId } = req.params; 244 + const userId = getCurrentUser(); 245 + const likeCount = await db.likes.count({ where: { postId } }); 246 + const isLiked = await db.likes.count({ where: { postId, userId } }) > 0; 247 + const html = `<html> 248 + <body> 249 + <button 250 + id="likeButton" 251 + className="${isLiked ? 'liked' : ''}" 252 + data-postid="${Number(postId)}"> 253 + ${likeCount} Likes 254 + </button> 255 + <script src="./frontend.js></script> 256 + </body>` 257 + res.text(html); 258 + }); 259 + ``` 260 + 261 + </Server> 262 + 263 + The browser will load that `<script>` which will attach the interactive logic: 264 + 265 + <Client> 266 + 267 + ```js 268 + document.getElementById('likeButton').onclick = async function() { 269 + const postId = this.dataset.postId; 270 + if (this.classList.contains('liked')) { 271 + // ... 272 + } else { 273 + // ... 274 + } 275 + }; 276 + ``` 277 + 278 + </Client> 279 + 280 + This works but leaves a few things to be desired. 281 + 282 + For one, you probably don't want the frontend logic to be "global"--ideally, it should be possible to render multiple Like buttons, each receiving its own data and maintaining its own local state. It would also be nice to unify the display logic between the template in the HTML and the interactive JavaScript event handlers. 283 + 284 + We know how to solve these problems. That's what component libraries are for! Let's reimplement the frontend logic as a declarative `LikeButton` component: 285 + 286 + <Client> 287 + 288 + ```js 289 + function LikeButton({ postId, likeCount, isLiked }) { 290 + function handleClick() { 291 + // ... 292 + } 293 + 294 + return ( 295 + <button className={isLiked ? 'liked' : ''}> 296 + {likeCount} Likes 297 + </button> 298 + ); 299 + } 300 + ``` 301 + 302 + </Client> 303 + 304 + For simplicity, let's temporarily drop down to purely client-side rendering. With purely client-side rendering, our server code's job is just to pass the initial props: 305 + 306 + <Server> 307 + 308 + ```js {8-16} 309 + app.get('/posts/:postId', async (req, res) => { 310 + const { postId } = req.params; 311 + const userId = getCurrentUser(); 312 + const likeCount = await db.likes.count({ where: { postId } }); 313 + const isLiked = await db.likes.count({ where: { postId, userId } }) > 0; 314 + const html = `<html> 315 + <body> 316 + <script src="./frontend.js></script> 317 + <script> 318 + const output = LikeButton(${JSON.stringify({ 319 + postId, 320 + likeCount, 321 + isLiked 322 + })}); 323 + render(document.body, output); 324 + </script> 325 + </body>` 326 + res.text(html); 327 + }); 328 + ``` 329 + 330 + </Server> 331 + 332 + Then the `LikeButton` can appear on the page with these props: 333 + 334 + <Client> 335 + 336 + ```js {1} 337 + function LikeButton({ postId, likeCount, isLiked }) { 338 + function handleClick() { 339 + // ... 340 + } 341 + 342 + return ( 343 + <button className={isLiked ? 'liked' : ''}> 344 + {likeCount} Likes 345 + </button> 346 + ); 347 + } 348 + ``` 349 + 350 + </Client> 351 + 352 + This makes sense, and is in fact exactly how React used to be integrated in server-rendered applications before the advent of client-side routing. You'd need to write a `<script>` to the page with your client-side code, and you would write another `<script>` with the inline data (i.e. the initial props) needed by that code. 353 + 354 + Let's entertain the shape of this code for a little bit longer. There's something curious happening: the backend code clearly wants to *pass information* to the frontend code. However, the act of passing information is again *stringly-typed!* 355 + 356 + What's going on here? 357 + 358 + <Server> 359 + 360 + ```js {5-13} 361 + app.get('/posts/:postId', async (req, res) => { 362 + // ... 363 + const html = `<html> 364 + <body> 365 + <script src="./frontend.js></script> 366 + <script> 367 + const output = LikeButton(${JSON.stringify({ 368 + postId, 369 + likeCount, 370 + isLiked 371 + })}); 372 + render(document.body, output); 373 + </script> 374 + </body>` 375 + res.text(html); 376 + }); 377 + ``` 378 + 379 + </Server> 380 + 381 + What we seem to be saying is: have the browser load `frontend.js`, then find the `LikeButton` function in that file, and then pass this JSON to that function. 382 + 383 + So what if could *just say that?* 384 + 385 + <Server> 386 + 387 + ```js {1,8-12} 388 + import { LikeButton } from './frontend'; 389 + 390 + app.get('/posts/:postId', async (req, res) => { 391 + // ... 392 + const jsx = ( 393 + <html> 394 + <body> 395 + <LikeButton 396 + postId={postId} 397 + likeCount={likeCount} 398 + isLiked={isLiked} 399 + /> 400 + </body> 401 + </html> 402 + ); 403 + // ... 404 + }); 405 + ``` 406 + 407 + </Server> 408 + 409 + <Client glued> 410 + 411 + ```js {1,3} 412 + 'use client'; // Mark all exports as "renderable" from the backend 413 + 414 + export function LikeButton({ postId, likeCount, isLiked }) { 415 + function handleClick() { 416 + // ... 417 + } 418 + 419 + return ( 420 + <button className={isLiked ? 'liked' : ''}> 421 + {likeCount} Likes 422 + </button> 423 + ); 424 + } 425 + ``` 426 + 427 + </Client> 428 + 429 + We're taking a conceptual leap there but stick with me. What we're saying is, these are still two separate runtime environments--the backend and the frontend--but we're looking at them as a *single program* rather than as two separate programs. 430 + 431 + This is why we set up a *syntactic connection* between the place that passes the information (the backend) and the function that needs to receive it (the frontend). And the most natural way to express that connection is, again, a plain `import`. 432 + 433 + Note how, again, importing from a file decorated with `'use client'` from the backend doesn't give us the `LikeButton` function itself. Instead, it gives a *client reference*--something that we can turn into a `<script>` tag under the hood later. 434 + 435 + Let's see how this works. 436 + 437 + This JSX: 438 + 439 + ```js {1,6-10} 440 + import { LikeButton } from './frontend'; // "/src/frontend.js#LikeButton" 441 + 442 + // ... 443 + <html> 444 + <body> 445 + <LikeButton 446 + postId={42} 447 + likeCount={8} 448 + isLiked={true} 449 + /> 450 + </body> 451 + </html> 452 + ``` 453 + 454 + produces this JSON: 455 + 456 + ```js {7-14} 457 + { 458 + type: "html", 459 + props: { 460 + children: { 461 + type: "body", 462 + props: { 463 + children: { 464 + type: "/src/frontend.js#LikeButton", // A client reference! 465 + props: { 466 + postId: 42 467 + likeCount: 8 468 + isLiked: true 469 + } 470 + } 471 + } 472 + } 473 + } 474 + } 475 + ``` 476 + 477 + And this information--this *client reference*--lets us generate the `<script>` tags that load the code from the right file and call the right function under the hood: 478 + 479 + ```html 480 + <script src="./frontend.js"></script> 481 + <script> 482 + const output = LikeButton(${JSON.stringify({ 483 + postId, 484 + likeCount, 485 + isLiked 486 + })}); 487 + // ... 488 + </script> 489 + ``` 490 + 491 + In fact, we also have enough information that we can run the same function on the server to pregenerate the initial HTML, which we lost with client rendering: 492 + 493 + ```html {1-4} 494 + <!-- Optional: Initial HTML --> 495 + <button class="liked"> 496 + 8 Likes 497 + </button> 498 + 499 + <!-- Interactivity --> 500 + <script src="./frontend.js"></script> 501 + <script> 502 + const output = LikeButton(${JSON.stringify({ 503 + postId, 504 + likeCount, 505 + isLiked 506 + })}); 507 + // ... 508 + </script> 509 + ``` 510 + 511 + Prerendering the initial HTML is optional, but it works using the same primitives. 512 + 513 + Now that you know how it *works*, look over this code one more time: 514 + 515 + <Server> 516 + 517 + ```js {1,8-12} 518 + import { LikeButton } from './frontend'; // "/src/frontend.js#LikeButton" 519 + 520 + app.get('/posts/:postId', async (req, res) => { 521 + // ... 522 + const jsx = ( 523 + <html> 524 + <body> 525 + <LikeButton 526 + postId={postId} 527 + likeCount={likeCount} 528 + isLiked={isLiked} 529 + /> 530 + </body> 531 + </html> 532 + ); 533 + // ... 534 + }); 535 + ``` 536 + 537 + </Server> 538 + 539 + <Client glued> 540 + 541 + ```js {1,3} 542 + 'use client'; // Mark all exports as "renderable" from the backend 543 + 544 + export function LikeButton({ postId, likeCount, isLiked }) { 545 + function handleClick() { 546 + // ... 547 + } 548 + 549 + return ( 550 + <button className={isLiked ? 'liked' : ''}> 551 + {likeCount} Likes 552 + </button> 553 + ); 554 + } 555 + ``` 556 + 557 + </Client> 558 + 559 + If you set aside your existing notions of how the backend and the frontend code should interact, you'll see that there's something special happening here. 560 + 561 + The backend code *references* the frontend code by using an `import` with `'use client'`. In other words, it express a direct connection *within the module system* between the part of the program that *sends* the `<script>` and the part of the program that lives *within* that `<script>`. Since there is a direct connection, it can be typechecked, you can use "Find All References", and all tooling is aware of it. 562 + 563 + Like `'use server'` before it, `'use client'` makes the connection between the server and the client *syntactic*. Whereas `'use server'` opens a door from the client to the server, `'use client'` opens a door from the server to the client. 564 + 565 + It's like two worlds with two doors between them. 566 + 567 + --- 568 + 569 + ### Two Worlds, Two Doors 570 + 571 + As you can see, `'use client'` and `'use server'` should not be seen as ways to "mark" code as being "on the client" or "on the server". That is not what they do. 572 + 573 + Rather, they let you *open the door* from one environment to the other: 574 + 575 + - **`'use client'` exports client functions _to_ the server.** Under the hood, the backend code sees them as references like `'/src/frontend.js#LikeButton'`. They can be rendered as JSX tags and will ultimately turn into `<script>` tags. 576 + - **`'use server'` exports server functions _to_ the client.** Under the hood, the frontend sees them as `async` functions that call the backend via HTTP. 577 + 578 + These directives express the network gap *within* your module system. They let you describe a client/server application as a *single program spanning two environments.* 579 + 580 + They acknowledge and fully embrace the fact that these environments don't share any execution context--this is why neither `import` executes any code. Instead, they only let one side *refer* to code on the other side--and pass information to it. 581 + 582 + **Together, they let you "weave" the two sides of your program by creating and composing [reusable abstractions with logic from both sides.](/impossible-components/)** But I think the pattern extends beyond React and even beyond JavaScript. Really, this is just RPC at the module system level with a mirror twin for sending more code to the client. 583 + 584 + The server and the client are two sides of a single program. They're separated by time and space so they can't share the execution context and directly `import` each other. The directives "open the doors" across time and space: the server can *render* the client as a `<script>`; the client can *talk back* to the server via `fetch()`. But `import` is the most direct way to express that, so directives make it happen. 585 + 586 + Makes sense, doesn't it?