React 19 - Part 2: The Code; How it works under the hood!
In the previous article, we delved into the backstory of how it all came to be: the numerous iterations and the twists and turns that ultimately led us to the final R19 prototype. Now, it's time to discuss the code, the setup, and how it all operates under the hood.
IMPORTANT:
All of the code snippets provided here are simplified versions of the actual code. The real code can be a bit more complex, and sometimes too verbose to be displayed in its entirety. If you're comfortable delving into the code, you can explore everything in the R19 repository.
#Build Step
It all begins with the build step. The primary focus of this project was to create a framework that is both fast and simple to understand. For the fast aspect, I aimed to transpile and bundle the code ahead of time. This served as the initial step so that we could concentrate on the simple to understand aspect during runtime.
The build script utilizes Bun as the bundler. With an API very similar to esbuild but faster and fully integrated with our runtime, it was an obvious choice. However, this doesn't imply that the process was easy or straightforward. The few differences between the two bundlers made the process slightly more challenging than anticipated, but ultimately, it was worthwhile.
The result was a 50-line-ish build script. This script transpiles the code, bundles it, incorporates all necessary React 19 specifics, and outputs everything to the build
directory.
#The Pages
Firstly, let's address what needs to be built. In this case, it's essentially everything. Therefore, we categorize the files in the src
directory into pages
, components
, and assets
. The latter encompasses anything that is not a .ts
or .tsx
file, allowing us to support files like favicon.ico
, robots.txt
, static .css
style sheets, and so on.
Next, we move on to the pages. Here, we define our custom plugin, the rsc-register
. This plugin will be invoked for every import
call during the source loading process. This plugin plays an essential role in the React 19 flow, performing essentially the same function as the React "official" bundler plugins. It handles the RSC
and RSA
referencing. However, I'll refrain from delving deeper into this now; we'll discuss them later.
NOTE:
There isn't a distinct separation between the "pages" and "components" builds. We only need to split them up due to how we handle imports. If we applied the plugin universally, we wouldn't be able to differentiate between
import
s and the actual source code.
#The RSC Register
This is where the magic happens. Every file imported by our pages that matches the filter /\.ts?$/
(which essentially ensures we handle only local imports) will be processed by this plugin. Here's how it works: We read the file, check if there's a use client
or use server
directive. If there's none, we let it be bundled, resulting in the content being inlined. If there's a directive but no exports, we still let it be bundled. However, if there's a directive and exports, we create a reference to it.
IMPORTANT:
Here, we utilize the
Bun.Transpiler
to scan the file and list theexports
. This is a useful feature of the Bun bundler, but it can also be achieved in Node.js usingacorn-loose
, as the official React plugins do.
Finally, we register the reference. This is a new concept introduced by React 19. It's the way to inform the server or client that there's a client component needing import or render or a server action requiring invocation and state1 update. Below is a simplified explanation of how this is accomplished through a helper function:
Yeah... It's not pretty at all. Let me try to explain it a little bit better.
-
This would be the output for a server action reference with a default export:
CAUTION:
The actual code should never be shipped to the client. Hence, we utilize a mock function that throws an error when called. This prevents shipping the code to the client while informing it about the existence of a server action there. React handles this through the reference, but attempting to call the function will result in an error.
-
And this would be the output for a client component reference with a named export:
NOTE:
We cannot allow the code to be bundled. Otherwise, a React server renderer would attempt to render a component with a hook, which would break a rule of hooks.
#The Components
Handling components is relatively straightforward. We simply need to bundle them, and that's it! There are no special cases to consider; we bundle them and output the result to the build
directory.
#The Assets
This category encompasses essentially anything else. Here, we could implement optimizations such as using sharp
to compress images and postcss
to minify and bundle styles. However, for now, we'll stick to copying the files to the build
directory.
NOTE:
While I'm not entirely sure about how other bundlers handle this, Bun provides a convenient API for handling assets. It automatically replaces
import
calls with the correct path, alleviating any concerns about path management.
#How the Runtime Works
Now, let's delve into the real deal. Everything explained above was just setting the stage (albeit one of the most unique parts of the project). We're now moving on to discuss the runtime: how RSC
and RSA
are handled, how server state is managed, and more.
Here, we have two servers: rsc.ts
and ssr.ts
, each serving a different purpose, yet both are essential to each other.
NOTE:
There's ways to make all of this with one server, as Next.js does, but as Josh said: "the way next does this from a single process is to use webpack layers. The server bundle has both the RSC and SSR modules in the same graph but there is a copy for RSC and a separate copy for SSR for react packages and client components only see the SSR one and server components only see the RSC one."
I wanted to keep it simple, so I decided to split them up. (Also I have no idea how any of those other things work :))
#The RSC Server
This is the server that handles RSC
payloads—the essence of React 19. It listens for both GET and POST requests on "/*". The GET request renders the page to an RSC
stream:
It's responsible for managing server actions, decoding props, gathering results, and updating the UI with the new state. Additionally, it returns the new RSC
stream.
#renderToPipeableStream
This is an interesting part of the code—it's the function responsible for rendering the RSC stream. While it may seem a bit complex when delving into the details (source), it's not too difficult to understand from the outside. Essentially, it creates a pipeable stream to render the RSC stream built from the ReactNode
tree.
#The SSR Server
This server acts as the front-facing server, handling GET requests from users (although, please don't). Hypothetically speaking, this server forwards the request using the node:http
module, resulting in a stream—specifically, a NodeStream
—which can be handled by "react-server-dom-esm/client.node".
Here, Layout
refers to a global root shell used to wrap the RSC stream, containing setup such as head meta tags and global scripts. The Root
represents the actual RSC stream, wrapped in a use
hook, constituting the page content.
"renderToPipeableStream" in this context shares the same name as seen on the RSC server, but it's a distinct function. This one comes from "react-dom/server.node" and is responsible for taking the RSC, layout, props, and other necessary elements and rendering them to an HTML string. (Believe me, it took me a while to figure this out)
And guess what? We're almost done!
#Extras
Do you think those static paths and fixed module base URLs are a little bit too much? Yeah, me too. But instead of fixing them, I had another idea. What if we exported HTML? We already know how to render it, so why not? Anytime someone requests a page without any props, we'll just send the HTML. This way, we can have a static site with a dynamic server, client components, server actions, be as quick as possible, and still have good SEO.
Please, could you stop laughing? I know it's not the best solution, but it's a solution. And it works. And it's fast. And it's simple. And it's easy to understand. Well, it's a solution.
With a few adjustments to our ssr.ts
, we can now serve the HTML files, and we're done!
We have a full stack: server-side rendered, client-side rendered, server actions, and static pages, all in one place, all in one project, all in one... well, you get it.
#Conclusion
This was a long one, but I hope you enjoyed it. I hope you learned something. I hope you're excited about the future of React and web development. Personally, this was a great journey. I learned a lot. And besides all the PHP jokes, you know what? This seems like a really, and I mean really powerful model to think about web development. I'm really excited to see where it goes. I do think everyone, even those working at React, might have some pushbacks and problems with the size or how slow things can be. But you know what's even slower? Not trying at all.
See you all in my next months-long dubious quality project!
P.S.: Delba dropped this banger of a visualizer for the React 19 references, and how client components behave in each environment. It's a must-see!