Server Components
Loading "Intro to Server Components"
Run locally for transcripts
The vast majority of React applications have both of these issues:
- Data is subject to either cascading waterfalls or prop drilling.
- JavaScript is sent to the client to hydrate components that are not interactive.
Depending on your situation, these two issues can be minimal and not worth
solving or they can be a major pain point. For most of you, it's probably the
latter.
React server components solves both of these problems. By pushing the UI we
generate to the server, we reduce the JavaScript sent to the client down to only
the interactive bits and we also enable data fetching from the components
directly.
The idea behind RSCs is conceptually simple. Instead of requesting JSON data and
handing that off to our components to generate the UI, we request the UI itself.
Let's compare initial render of a SPA that uses JSON with a SPA that uses RSCs:
Here's a bullet-point text version of this flowchart:
-
User goes to site
- Browser requests document
- Server responds with document
- Browser renders loading spinner
- Server responds with document
- Browser requests client code
- Server responds with client code
- Browser updates UI components
- Server responds with client code
- Browser requests data
- Server generates data response
- Browser sends JSON
- Browser updates UI with JSON data
- Browser sends JSON
- Server generates data response
- Browser requests document
Here's a bullet-point text version of the flowchart:
-
User goes to site
- Browser requests document
- Server responds with document
- Browser renders Suspense fallback
- Server responds with document
- Browser requests JSX payload
- Server generates Serialized JSX with
react-server-dom-esm/server.renderToPipeableStream
- Server streams Serialized JSX
- Browser renders streamed UI with
react-server-dom-esm/client.createFromFetch
- Browser requests client component code
- Server responds with client component code
- Browser hydrates client components
- Server responds with client component code
- Browser requests client component code
- Browser renders streamed UI with
- Server streams Serialized JSX
- Server generates Serialized JSX with
- Browser requests document
You'll notice these two flows are pretty similar. The biggest difference is in
where we generate the UI.
In the SPA case, UI = data + components on the client.
In the RSC case, UI = data + components on the server.
Of course, there are some parts of our UI that need to be interactive and
that requires some of our code to be sent to the client. We'll cover this part
in a future exercise. This is important because it's the reason React needs its
own serialization format and can't just use HTML.
RSC Format
Typically when you server render React, you render a string of HTML. However,
React Server Components need to have a good mechanism for mixing components for
client-side interactivity with components that are rendered on the server only.
Additionally, React Server Actions also necessitate a way to send a reference
to the client-side component that will be hydrated.
To top it off, this all needs to be done in a way that allows for streaming the
UI to the client in any order.
So React Server Components use a custom serialization format that looks like
this:
1:D{"name":"App","env":"Server"}
0:{"returnValue":null,"root":"$L1"}
2:I["/ship-search.js","ShipSearch"]
5:I["/ship-details-pending.js","ShipDetailsPendingTransition"]
6:I["/error-boundary.js","ErrorBoundary"]
8:"$Sreact.suspense"
a:I["/img.js","ShipImg"]
3:D{"name":"SearchResults","env":"Server"}
4:D{"name":"SearchResultsFallback","env":"Server"}
4:[["$","li","0",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","1",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","2",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","3",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","4",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","5",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","6",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","7",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","8",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","9",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","10",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","11",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}]]
7:D{"name":"ShipError","env":"Server"}
7:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","img",null,{"src":"/img/broken-ship.webp","alt":"broken ship"}]}],["$","section",null,{"children":["$","h2",null,{"children":"There was an error"}]}],["$","section",null,{"children":["There was an error loading \"","0268fc4817ad1","\""]}]]}]
9:D{"name":"ShipFallback","env":"Server"}
9:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","$La",null,{"src":"/img/ships/0268fc4817ad1.webp?size=200","alt":"0268fc4817ad1"}]}],["$","section",null,{"children":["$","h2",null,{"children":"Loading..."}]}],["$","div",null,{"children":["Top Speed: XX"," ",["$","small",null,{"children":"lyh"}]]}],["$","section",null,{"children":["$","ul",null,{"children":[["$","li","0",{"children":[["$","label",null,{"children":"loading"}],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"}]]}]]}],["$","li","1",{"children":[["$","label",null,{"children":"loading"}],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"}]]}]]}],["$","li","2",{"children":[["$","label",null,{"children":"loading"}],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"}]]}]]}]]}]}]]}]
b:D{"name":"ShipDetails","env":"Server"}
1:["$","div",null,{"className":"app","children":[["$","div",null,{"className":"search","children":["$","$L2",null,{"search":"m","results":"$L3","fallback":"$4"}]}],["$","$L5",null,{"children":["$","$L6",null,{"fallback":"$7","children":["$","$8",null,{"fallback":"$9","children":"$Lb"}]}]}]]}]
c:I["/ship-search.js","SelectShipLink"]
3:[["$","li","Bomber",{"children":["$","$Lc",null,{"shipId":"5c13d8b28a14a","highlight":false,"children":[["$","$La",null,{"src":"/img/ships/5c13d8b28a14a.webp?size=20","alt":"Bomber"}],"Bomber"]}]}],["$","li","Diplomatic Vessel",{"children":["$","$Lc",null,{"shipId":"6f375578ead88","highlight":false,"children":[["$","$La",null,{"src":"/img/ships/6f375578ead88.webp?size=20","alt":"Diplomatic Vessel"}],"Diplomatic Vessel"]}]}],["$","li","Mining Ship",{"children":["$","$Lc",null,{"shipId":"627c497212456","highlight":false,"children":[["$","$La",null,{"src":"/img/ships/627c497212456.webp?size=20","alt":"Mining Ship"}],"Mining Ship"]}]}],["$","li","Medical Ship",{"children":["$","$Lc",null,{"shipId":"0268fc4817ad1","highlight":true,"children":[["$","$La",null,{"src":"/img/ships/0268fc4817ad1.webp?size=20","alt":"Medical Ship"}],"Medical Ship"]}]}]]
b:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","$La",null,{"src":"/img/ships/0268fc4817ad1.webp?size=200","alt":"Medical Ship"}]}],["$","section",null,{"children":["$","h2",null,{"children":"Medical Ship"}]}],["$","div",null,{"children":["Top Speed: ",2," ",["$","small",null,{"children":"lyh"}]]}],["$","section",null,{"children":["$","p",null,{"children":"NOTE: This ship is not equipped with any weapons."}]}]]}]
This is not something you would want to write by hand. Instead, you'll use the
react-server-dom-esm/server
's renderToPipeableStream
function to generate
this for you. Then on the client-side, react-server-dom-esm/client
's
createFromFetch
will take care of converting this format into React elements
that can be rendered by React in the browser.The
react-server-dom-esm
package is one of many such packages for generating
and consuming React Server Components.To explore this format further, you can check out this blog
post
that goes in depth on the format and the accompanying
tool that can help you visualize different
aspects of the format.
Putting it together
So on the server side, you have some code that generates the serialized JSX in
the RSC format and on the client side, you have some code that converts the
serialized JSX into React elements:
import { createElement as h } from 'react'
import { renderToPipeableStream } from 'react-server-dom-esm/server'
// some server code...
app.get('/rsc', context => {
const { pipe } = renderToPipeableStream(h(App))
pipe(context.env.outgoing)
return RESPONSE_ALREADY_SENT
})
import { createFromFetch } from 'react-server-dom-esm/client'
// some client code...
const responsePromise = fetch('/rsc')
const ui = await createFromFetch(responsePromise)
createRoot(document.getElementById('root')).render(ui)
Now you combine this with some nice suspense code on the client for handling the
loading state and you've got yourself a nice RSC setup.
In this world, it doesn't matter how many components you have or how heavy those
dependencies are. They'll never get sent over the wire. Just the UI that they
generated.
In some situations, it's possible sending the data and components will be less
than sending the generated UI. But the size of the payload is only one aspect
to consider. Parsing and executing JavaScript on the client requires more
resources than handling the stream of RSCs. Additionally, RSCs enable
composition of data requirements and UI generation in a way that is not
possible with JSON data.