My guess at how React Server Component(RSC) works internally
Before I dive into the source code of RSC(React Server Components), here is me trying to implement something similar to SRC. Later I could compare it to the real implementation and strengthen my understanding.
Before you read the post, I sugges you watch the intro video from React team, Iāll try to mimic the demo there. Also the official React Server Comonents demo uses the official APIs from React, in our demo, we donāt use them.
Iāll split the full journey into several milestones to make it easier to follow, all the code are on github & stackblitz.
1 - Issues of client-side rendering
see the full code on github PR
Letās start with an app of client-side rendering.
Above is a React app that:
- has all components bundled in one
- fetches data through API and does client-side rendering
- parses markdown on client by marked
- shows loading indicator by Suspense
This approach is pretty standard, it should be fine, Iāve been doing this way for a long time. But still there are some issues :
- components are all bundled together, even when not rendered on initial load
- the dependency of markdown parser is too much
- the API exposing is tedious
How can we improve ? Letās first start with the dependency issue.
2 Manually split component into client part & server part
see the full code on github PR
To address the dependency issue, if we donāt parse the markdown on client, we have to parse it on server. This means we need to run PostDetail on server and somehow communicate the response with cilent.
Here is what we are going to do:
- manually split PostDetail into PostDetail.client & PostDetail.server
- PostDetail.client just pass down the props and query response from
/render
/render
will render PostDetail.server into JSON and send it back PostDetail.client renders the response
Since we know JSX is rendered to React.createElement()
calls, which returns some JSON, we can easily serialize the response with some special cases like symbols and built-in components.
By above steps, we have a rough Server Component working for us.
We managed to
- move markdown related dependencies to server
- remove data API endpoints
Open Chrome Dev Console to view the requests of /render
.
This looks great, right? But it doesnāt support nested components though, we cannot do the same to PostList, how can we improve?
3 Render Client Components in Server Components
see the full code on github PR
We couldnāt move PostList to server because it renders <Link/>
and <Link/>
needs DOM api which means it must be a Client Component
We can do following to address this issue
- when rendering Server Component, we replace Client Components with āLazyContainerā - it is just a string
- āLazyContainerā will be replaced with working client component -LazyContainer on client
- LazyContainer lazy loads the actual component (
Link
in our case) and renders
With this we are able to move PostList to a Server Component, Hooray! See the lazily loaded JavaScript from demo below.
But it is even more tedious now with more .client
and .server
, how can we make it less painful?
4 Automatically build Server Components
see the full code on github PR
We see how tedious it is to manually manage the client part of server component.
After splitting two components, we can observe that the client part of both server components are almost the same, it is just a loader component that communicates with /render
, we can merge them into one component.
Then we can modify the build scripts to do this automatically for us so that
- default components are Server Components, unless āuse clientā is declared
- no more naming of
.server.js
or.client.js
.
Now after building, client code use /public, server uses /built, components have the same name across the folders, as they originally are.
Below is the new app, we can see it works totally the same as before but the code is much cleaner with out importing anything as ā.clientā or ā.serverā.
5 Support nested Server Components & Suspense
see the full code on github PR
So far we couldnāt move page-level components List & Detail to Server Components because
- they have nested Server Components, but we only render one level in our code
- they have Suspense
Letās tweak the code a little bit so that
- nested Server Components can be supported (not directly rendered though, we only send down their client part and they will be rendered by another request. This creates a waterfall)
- Suspense is supported: this could be easily done because Suspense is built-in component
With this new demo, we can see the Suspenses be rendered nicely.
We donāt want the /render
to be called multiple times though, how can we improve?
6 Render Server Components and stream down response
With what weāve learnt from How progressive hydration works, we can try streaming down the responses in one request!
see the full code on github PR
The code is not complex actually, with previous episodes it is pretty natural to come this far. Open the Chrome Dev console to see that there is only 1 /render
call for each page navigation.
There seems to be some issues with streaming on stackblitz, so you cannot see the Suspense rendered properly, I suggest you clone the repo to try it out locally.
Summary
This is my guess at how React Server Componet works internally. We can see that it is more about the streaming, which we should rely on frameworks (Hello, Vercel!).
The code is actually not complex (well, a lot of tasks were undone though, like error handling .etc).
I hope it is not too far away from the real implementation. Anyway Iāll figure it out soon, stay tuned!
Want to know more about how React works internally?
Check out my series - React Internals Deep Dive!