Video Summary and Transcription
In this Talk, Kent C. Dodds introduces React Server Components (RSCs) and demonstrates how to build them from scratch. He explains the process of integrating RSCs with the UI, switching to RSC and streaming for improved performance, and the benefits of using RSCs with async components. Dodds also discusses enhancements with streaming and server context, client support and loaders, server component rendering and module resolution, handling UI updates and rendering, handling back buttons and caching, and concludes with further resources for diving deeper into the topic.
1. Introduction to React Server Components
Hey, React Summit! My name is Kent C. Dodds and I'm excited to give you this talk and now you understand React Server Components. We're going to build React Server Components from scratch. We're going to build a framework based on React Server Components, and this is one of the mechanisms that I typically use to help people understand the building blocks upon which they're building. Here are the rules, this is like a Legend of Zelda speedrun, you've got to have rules, so we're not going to use a bundler. We're not using any dependencies. I am assuming that you're already optimistic about RSEs, I'm not here to convince you that RSEs are awesome, you're willing to dive in for details later. You already know the basics of RSEs, so useClient is not a new concept for you. And then also you're smart enough to not try to do this in production, this, like I said, very sub-optimal. It's a single-page app, so we're going to start out with fully 100% single-page app, not even server-side generated or anything like that, just a server or a single-page app in the client.
Hey, React Summit! My name is Kent C. Dodds and I'm excited to give you this talk and now you understand React Server Components. Wish me luck! Now, if we were in person, I would ask you to wake up and stand up. If you're physically able, join us for some air squats. We're not in person, so I'm not going to make you do that. But if it's been a while since you've gotten your blood flowing, you should do that because your brain needs blood flow. We're going to skip that for today, though.
All right. So, back in December of 2020, when Server Components was announced, I remember thinking, feeling kind of funny about it. I said, everyone's super excited about React Server Components and I guess I am supposed to be, too, but I'm feeling really meh about it. Thing is, a few months ago, I would have been going bonkers over this stuff, but honestly, Remix solves the same problems already, so and then I had a thread of kind of why I was feeling the way I was feeling. Still a little bit optimistic, but just like I felt like the problems that Server Components were intended to solve didn't really apply to me as much. I was missing a couple of things that Server Components does that Remix won't be able to do without Server Components, and so please forgive me my hesitancy, but eventually I did come around to Server Components, and now I have actually built a framework based on Server Components. Mine isn't intended for production or anything, but yeah, this is kind of the transformation that I've had, and if you're familiar with this scene, this is where the hobbits make it back after their journey through Mordor and everything, and they're changed, and I kind of feel that way, too, after having delved into Server Components a bit, and now I can appreciate the value that there's there. I know maybe some of you are feeling this way, I don't understand React Server Components, and at this point I'm afraid to ask, but my job is to try and explain React Server Components in such a way that it's simple enough for you to understand. That's my goal, so wish me luck.
Alright, so we're going to build React Server Components from scratch. We're going to build a framework based on React Server Components, and this is one of the mechanisms that I typically use to help people understand the building blocks upon which they're building, so I don't expect you to actually do this, but hopefully by building a framework on top of React Server Components you have a much clearer distinction of what's React Server Components and what's a framework thing and whatever. So here are the rules, this is like a Legend of Zelda speedrun, you've got to have rules, so we're not going to use a bundler. Bundler would just distract us from the core idea of what is React Server Components. TypeScript as well, so we don't want to have any build tools at all, we're just going free without TypeScript, and not even JSX, so you're going to find some createElement as h, that's short for hyperscript, and yeah, so we're going to be using createElement, the createElement API directly. Luckily we're not spending a lot of time in the JSX stuff, so you're fine, we're not going to be doing any optimizations, there are plenty to be had, but this is not going to be an optimal thing, and we're also not using any dependencies. I don't want you to be distracted by all of the other extra stuff. We are basically no dependencies except for official React stuff, React Error Boundary, and Hano.js. A couple things I'm going to be taking for granted, I am assuming that you're already optimistic about RSEs, I'm not here to convince you that RSEs are awesome, you're willing to dive in for details later, so we're going to be glazing over a couple of things, and feel free to dive in later for details. You already know the basics of RSEs, so useClient is not a new concept for you. And then also you're smart enough to not try to do this in production, this, like I said, very sub-optimal.
Alright, with all of that established, here is the application that we're going to be working on for our example. It's a list detail view, it updates the URL and all of that stuff that you would expect. It's a single-page app, so we're going to start out with fully 100% single-page app, not even server-side generated or anything like that, just a server or a single-page app in the client.
2. Building React Server Components
The project looks a little bit like this, we've got our database, we have our server, and our UI directory. We make a fetch to our API with the initial location and use a hook to get the data we need. On the server side, we have a static file server and an API endpoint for getting the ship by its ID. RSCs involve calling an API to get server-rendered UI. We swap out the API call for RSCs and combine the data with the UI on the server. To achieve composability, we make some diffs, import react-server-dom-esm-slash-client, and use it in our code.
The project looks a little bit like this, we've got our database, we have our server, this is the Hano.js server that has an endpoint for us, and then our UI directory is all the UI-related stuff. And then, here's a little bit of an intro to the code that we're going to be working with and modifying over time. We've got our initial location and our initial data promise, so we're making a fetch to our API with our initial location that will have the ship ID and any search parameters. We serialize that, or deserialize that as JSON, and then we're using the use hook with that promise, and we're destructuring out the data that we need for our app, and then this is our replacement for JSX, this H thing, create element as H, passing these props to our app component. Specifics are not critically important for you on what that app component does at this point. Also, because we're doing native stuff all over the place, we have an import map in our index HTML, so when we say import React and React DOM and React DOM client, we're pulling all of these from ESMSH, we're using the React 19 beta, and yeah, hopefully in the near future this will just be regular React. And then finally, our server side, we've got a static file server for all of our files, and then we have an API endpoint for getting the ship by its ID. Also, we're going to grab the search terms, so this is basically all of the data that you need for this page. That includes our ship ID, the search, the ship, and the ship results, which we send to the client on that initial page load. And then, we also have just kind of like a catch-all where we'll send that index HTML. So typical SPA situation without SSG.
Okay, great. So now, let's talk about it. Let's go from our API to RSC. So when we load the app, we're making a request to go get API slash ship ID with the search term that will give us all the data we need for our app. Ship ID, here's our search, and then here's the ship and here's the results for the ships that match this search term. This is pretty typical. Maybe you do a couple of different API calls, but ultimately you call an API to go get your data. So RSCs are actually not an enormously different thing. Instead of calling an API to get data, you're going to call an API to get RSCs. Now, I want to be clear that you don't have to do this via like a server interaction at runtime. You can do this at build time if you wanted to do a static site generation sort of thing, but we're going to be doing this at runtime. So our job is to swap out the slash API for slash RSC, and instead of getting the data here and then combining the data with the UI on the client, we're actually going to combine the data with the UI on the server and then just send the UI. Things get a little bit interesting when we want to compose the interactive bits with the non-interactive bits, and so that's why we're not just saying, hey, go get me the HTML and inner HTML everything. So we are going to be going a little bit above what you might think is the simplest way to do this, but the reason is because we want to have nice composability with our client-side code, which we'll get to later. So here's how we accomplish this. This is a bunch of diffs that we're going to be making. So first of all, for the client-side aspect, we've got our react-server-dom-esm-slash-client. We're going to need to import this, so that's why we're adding it to our import map, and this is going to be responsible for some aspect of this. And then here's where we're actually using that.
3. Integrating RSCs with the UI
We no longer pull in the app and switch from an initial data promise to an initial content promise. On the server side, we use server DOM ESM/server to serialize our app and React elements into something that can be sent over the wire. The API call is now RSC, and instead of data, we use props. This gives us pipe, which we use to send the response.
So we get our UI slash index, and we're pulling create from fetch from that module. And this is a react-server or a react package that they haven't actually published. So you'll notice in here that we're pulling it from my namespace on NPM, and the reason is because this is very suboptimal, so this is pretty much just for demos like mine.
One thing you'll notice here is we're not pulling in the app. That is kind of interesting. We're no longer pulling in the app and all of the other stuff that the app is importing. We'll take a look at what that means here in a bit. We also are switching from an initial data promise to an initial content promise. So we make this fetch request to slash RSC with that initial location, so that has the ship ID and the search. And then we pass that fetch promise into create from fetch. That gives us a new promise that we're then going to pass into use, and interestingly, that returns us something that is renderable.
On the server side, we're going to be pulling in create element from React. This is like you do this with SSR, but what is new is it's rendered a pipeable stream coming from server DOM ESM slash server. So this is going to be responsible for taking our app and the React elements that we create with it and serializing it into something that can be sent over the wire. So instead of slash API, we're now slash RSC, and instead of data, we're now props. That's really the only change there. It's just more clear that this is props that we're passing to our app component, which we're rendering on the server. This is going to give us pipe, and then we pipe that response through the outgoing response here.
4. Switching to RSC and Streaming
Instead of slash API, we now use slash RSC and instead of data, we use props. This switch allows us to stream RSC content instead of rendering JSON. Streaming is optional but has its benefits.
So instead of slash API, we're now slash RSC, and instead of data, we're now props. That's really like the only change there. It's just more clear that this is props that we're going to be passing to our app component, which we're rendering on the server. This is going to give us pipe, and then we pipe that response through the outgoing response here. That's actually pretty much it. So you're just switching from rendering JSON to streaming this RSC content. And actually, technically you don't need to stream it. We'll talk about why streaming is nice in a second, but you don't have to if you don't want to.
5. React Server Components and Async Components
We switch from sending JSON to sending an RSC payload, no longer sending UI code to the client. Instead, we request a stream-ready version of React elements on the server. With React server components, we can have async components and co-locate data with the UI that requires it.
So that's basically it. Now we've got RSCs. We switch from sending JSON to sending an RSC payload, and we're no longer sending any UI code to the client anymore. We don't need to. At least our application UI code. It doesn't need to go to the client, which is cool. And instead what we request, before we were requesting data, and now we're requesting this fancy, almost looks like JSON but not quite, stream-ready sort of stuff.
So here's the way that I think about it. On the server, we create an app element. And then we pass that in to pipeable stream, and that generates this fancy-looking, stream-ready, serialized version of those React elements. And that gets fed into the create from fetch thing that is on React server DOM ESM. Create from fetch turns it into our React elements. So type div, props, such and such, yada yada. So that's the flow of how things go when we move it over to React server components.
And now that we have this, we can actually do a couple of cool things. So for one, we have async components. So with that, we can go to our server, and you know how we're grabbing the ship and the ships right here and sticking those in our props and then sending those off to the app? Well, now, we don't need to send those anymore, because we're running on the server. So the components that need that information can go get it themselves, and all we need is this global context stuff, this ship ID in the search. And then that can be used to retrieve the ships in the search ships and stuff. And so here, if we look at our UI app, we're no longer accepting those props. We're no longer forwarding those props. And then in the individual components, we can say, hey, I can be an async component now, because I'm on the server. And I can get the ship. And right here, I can search the ship. And that actually drastically this is one of the things that I was missing about React server components was the co-location of data to the UI that requires the data. We've been chasing that dream for a long time. And Remix has that with loaders and actions. But that is at a route level, which is close enough lots of the time.
6. Enhancements with Streaming and Server Context
Remix has loaders and actions at a route level. Streaming can be achieved with React server components by wrapping UI parts in suspense boundaries. Server context eliminates the need to pass props all over the place.
And Remix has that with loaders and actions. But that is at a route level, which is close enough lots of the time. But being able to do it right into the component, that has some enormous implications, like distributing it on NPM, for example. But we're not going to go too deep into that.
Let's continue, because I got a lot of more stuff I want to show you. So let's talk about streaming. Now, what we have right now is when I hit the page, it'll take as long as it takes to get all of this stuff before I can show any of it, which is a pain. So the cool thing is we can actually stream this stuff. Now that we have React server components. And so here we can just wrap the different parts of my UI in suspense boundaries. And once you have that in place, now we'll have the different suspense boundaries pop into place. And so anything that isn't wrapped in a suspense boundary or once that area of the app resolves the stuff it's waiting for, we can display that right away.
Another quick thing, a quick win that you might have heard is server context. And so here before, we had these props, the ship ID and search that we had to pass to our app component. Now if you're using async local storage in Node.js, then you can stick all of that data inside of this special ships data storage and then render your app. And then all of the different components can retrieve that from the ship data storage, kind of like context and get those values. So you no longer have to pass those props all over the place, which I think is a win for sure.
7. Enhancements with Client Support and Loader
Now if you're using async local storage in Node.js, you can store data in the ship data storage, eliminating the need to pass props everywhere. Let's talk about adding use client support. We need a mechanism to render server-side components and load client-side UI components. We use a loader to convert module content and serialize it as references to render specific components.
Now if you're using async local storage in Node.js, then you can stick all of that data inside of this special ships data storage and then render your app. And then all of the different components can retrieve that from the ship data storage, kind of like context and get those values. So you no longer have to pass those props all over the place, which I think is a win for sure.
Okay, moving on, because we're still we've got lots more to talk about. Let's talk about adding use client support. Right now, if I made an error of some kind, it's going to blow up on me. And I could probably handle this better, even just myself. But I want to handle this declaratively with error boundaries. And error boundaries are a piece of UI logic. So we're storing the error in our error boundary state. And so we need to have some solution that allows us to have client side code. Now you remember, we don't have any client side components going on here except what's in our index. And so we need to have some mechanism for saying, Hey, yeah, render this stuff on the server, but I need you to have little slots of places where UI components can be loaded in. And I need to know how to load those things in. So here's the interesting thing about that is that you cannot have your server components render those for various reasons. They live in different environments. And so because of that, what we end up doing is, at least in this example, you would use a bundler for this. But in our example, we can actually use a loader. And so we have this register RSC loader, which is right here. It registers our RSC loader. And our RSC loader, I'm not going to dive too deep into this. Basically it converts any, like as we're importing modules, it's going to pipe through our loader right here. And if we decide we can convert the string content of that module into something else so that our RSC components don't actually render the client components and can instead serialize it to be a reference to like, Hey, you need to render this particular component. And so here I'm importing this error boundary component, which has use client up here at the top. That is the directive that tells the loader, Hey, I need you to transform me in a special way. So if we look at what the console logs look like, if we console log the transform error boundary module. So this is what that module turns into. We said export star from react error boundary. And so it creates exports for each one of the exports of react error boundary. And then it calls this register client reference from our react server dom ESM server.
8. Server Component Rendering and Module Resolution
When a server component tries to render a special component, it queues in react server, Dom ESM. The server app is configured with a module base path to resolve different modules. The UI index passes the module base URL to fetch the required components for the UI to work.
And so that way, any time a server component tries to render one of these things, it's just like queuing in react server, Dom ESM to say, Hey, this is a special component. And I'll tell you what, I even can tell you where to find it, how to load this file. So when it needs to be resolved, then we can resolve it. And here, this is the error boundary export. Here's the error boundary context export, yada, yada, yada. And then in the app GS where this is our server component, what it sees is just this function. And so when it tries to render that, you don't actually call it. Remember, you're not calling your components that you're rendering react is. But when react sees this special component, it says, Oh, I'm not going to call that function. Instead, I'm going to just register that and we can see how we resolve that that registered module into something. So here we have our app, our server app. And in here, we're just going to tell it, here's the module base path. So you saw that that big, long file URL. This is going to say, get rid of everything that matches this module base path. So it's like go to the UI directory and delete everything off of before that. So that way, we can know how to resolve these different modules. And then in our UI index, we're going to pass module base URL so that it knows what URL it can hit to go and get those modules. And here's the really cool thing is that this allows us to have like infinity components. And then the payload, the RSE payload can actually say, here are the components you need. And then the browser can go and get just the components that it needs for this UI to work, which is, I think, pretty cool. And it allows us to solve our particular problem. This is a client component right there.
9. Handling UI Updates and Rendering
To handle UI updates, we make network requests to get updates for the current location and fetch the content from the backend. The root component manages state for the current location and the content, passing it as children to the router context. The navigate function updates the location and fetches the content, creating React elements that can be rendered on the client.
OK, great. So we've got just a tiny bit of time left. So we're probably going to have to dive a little bit deeper into a couple of these things. So for UI updates, as what we have right now is every time I do a search or every time I click on one of these items, I'm going to get a full page refresh. So we need to handle that because all of the data that's on this page comes from the server. So we need to make a network request to go get updates to get the details for the new ship that I've clicked on or the search results.
So the way that we accomplish this is in our UI. Well, first of all, we have our UI router. This is just a couple of utilities. It's a pretty typical context thing. We have this link handler that will handle link clicks and call navigate for us. But the real meat of all of this stuff is in our index.js. And so what's going on in here is we're still basically doing lots of the same stuff we were doing before. But the interesting thing is now our root component is managing some state for the current location and also managing state for the content that it's getting from the backend for that particular location. And that is what it ends up passing as children to the router context. So that ends up getting rendered. So nothing really changes yet. But now we have our navigate function. This is going to update our location to where we're trying to get to. And then it's going to fetch the content again. So this is fetching RSC content. And when that's finished, we can update the URL. And we're going to create from fetch. So take the RSC serialized stuff, turn it into React elements that we can render on the client. That's what the next content promise is. And then inside of a transition, we update the content promise. Because again, this is all running serially. None of this is running asynchronously except what's in the then callback here. And so we instantly say, hey, go ahead and start this transition. React does its magic stuff with transitions.
10. React Rendering and Pending UI
React can render new content instantly without full page refreshes. The pending UI is managed in the app UI using the ship details pending transition. The UI index now manages the next location using use deferred value, allowing comparison of current and pending UIs. Race conditions are handled by setting the latest nav symbol when navigating and ignoring non-latest navs in the then handler.
And then once this promise resolves right here, then React is like, great, here's your new content, and we can render that new content. So basically just call the server again, go get the new content. It's pretty cool. And so now we get some instant updates to our UI. We don't have to do full page refreshes. And it's all just happening because we say, hey, go get me some more content. And then we replace the content that is displayed.
All right. So let's talk about the pending UI really quick. So as I'm clicking around, you'll notice that we're not getting any sort of indication that things are going on. And the way that we do this is in our app UI, we now have this ship details pending transition. This is because this is a use client. So we've got to separate this because managing this pending state is going to happen as part of managing actual states. So that's why it needs to be in the client.
And then if we look at our UI index, this is where the meat of the router and these changes happen. Instead of managing the location, we're now going to just manage the next location. And our location will be derived from the next location using use deferred value. So that way we have two UIs. We have the UI that we had before and the UI that we're going to have next once everything resolves. And that way we can compare those two and determine whether we're currently pending. And with the next location, we can know, oh, where are we going? Okay. I know what thing is pending. So we can determine is the search pending or is the user or the ships the thing that's pending? And with that now, we have our nice pending UI all hooked into our router, which I think is kind of neat. But there are some race conditions. I just couldn't help myself. I had to show you this handling the race conditions. We just have the latest nav. We set that to a symbol when we start navigating. And in our then handler here, if we're not on the latest nav, we just don't do anything. And that solves race condition issues.
11. Handling Back Button and Caching
When hitting the back button, the URL updates but does not call the navigate function. Handling this involves setting the location, fetching the content, and implementing caching using sync external store. Entries in history are associated with keys in the cache, allowing easy retrieval of content promises. Navigating forward or backward generates new keys and associates them with the corresponding content promises.
You got to think about those things, people. Okay, great. So what about if we hit the back button? So I'm going to search for the Medix ship, and then I click on the Medix ship. And then if I hit the back button, nothing happens except the URL does update because the browser is going to handle that. But we need to listen to those events. Because that's not calling our navigate function. So all we need to do for that is our use effect right here is going to have this handle pop state for handling the pop state. And we're going to set the location, fetch the content for that location, and all is well, except all is not well.
Because we're making a new promise to fetch some data. So it's going to end up showing our suspense boundary. There's some interesting reasons why that happens, even though we're doing this inside of a transition. But it's important that we handle this case. And this brings in the caching. So we have to cache our stuff. So I've got this cache content stuff. This is using sync external store. Here is our store. It's an observable map that emits a change any time you change anything in the cache. And then we can consume that and trigger re-renders when our cache is updated.
But ultimately what we need to do is we need to associate the different entries in our history with a key and associate that key with a particular entry in our cache. And so we have our content cache. We're no longer tracking the promise. Instead we're tracking the key in history that we're currently on. We get our content promise from the cache at that content key. And then whenever we're changing history, we're going forward or backward, we're going to get the key from that state in history. We're going to generate a new key if one doesn't exist. And then based on that we're going to use the promise that is stored in our cache for that particular key. So any time we make a navigation, we're going to generate a new key and that key to that entry in our history and associate that key with the content promise. So that way as you're navigating and going forward and backward in history, we already have that in memory so we can just render that. Now caching is a whole big subject.
12. Concluding Remarks and Further Resources
A simple cache like this can be relatively straightforward. If you want to dive deeper, check out the Epic Web Workshop and Epic React version 2.0 on GitHub for more details.
It's complicated and stuff. But a simple cache like this, hopefully relatively straightforward. Something that you can dive into if you like.
All right. Actions. Oh man. We are running out of time. Oh good. No, we don't have time to talk about actions. I think I'm over time anyway.
If you want to dive in deeper, this is all from the Epic Web Workshop. This is Epic React version 2.0 that is going on Epic Web soon. So you can go through that on your own. It's open source on GitHub. And dive into some of this stuff if you want to get some of the details. And there's a whole bunch of other information in there. So feel free to dive in. And now you understand React server components. Congratulations. Thank you so much for giving me some of your time.
Comments