An Annotated Guide to React Server Components
React Server Components are a nuanced, sweeping addition to React's existing capabilities. This is my guide. #react
Read time: 13 minutesLast update:
The React team announced React Server Components this week with a talk, RFC, and demo. It is a lot to go through (especially for a holiday week), but I did. Here are my notes.
TL;DR
What are React Server Components?
They are an experimental, upcoming feature of React that lets you render Components only on the Server. This has a few implications; here are my top 2:
- RSCs have Zero client JS bundle impact, so they can greatly reduce your overall bundle size. Despite only rendering on the server, they don't have to be static; you can refetch Server Components and the server will stream updates down, without losing state in Client Components.
- You can directly query your database inside a Server Component without setting up GraphQL, API endpoints, or state management - and keep the query up to date as your serverside data changes. These queries are also faster because you save client-server roundtrips and waterfalls.
Note: They must be named with a
.server.js
extension and follow some constraints - mainly, they can't use state, effects, or DOM APIs.
What problems do React Server Components solve?
They solve for Good User Experience, Cheap Maintenance, and Fast Performance in React apps (as defined in the talk notes below). There are multiple benefits for both developers and users, and the size of benefit depends on the use case, which makes this difficult to fully explain.
However the headline opportunity here is to greatly reduce production React app bundle sizes (tests have seen as much as 29% reduction, but can easily be higher depending on your app) while not sacrificing modern app-like user experiences (this again has multiple meanings - demonstrated in the demo below).
Finally, React Server Components make it easy to create hybrid apps - giving you the power to pick a sliding scale between a fully clientside app or a fully serverside app and everything in between - and not have to do major rewrites to change paradigms as requirements change.
Annotated Talk With Timestamps
I will link to the timestamps with my commentary below, but you can view Dan Abramov and Lauren Tan's full talk here:
- 1:45 Dan introduces the main constraints that React is trying to solve for:
- Good User Experience - we want to orchestrate intentionally designed loading states. Items that should appear together must appear together, instead of jumping around on screen based on async API requests resolving.
- Cheap Maintenance - we want to optimize for change. Make it easy to delete and move code around from component to component or from client to server or from stateless to stateful. To do this, data requirements should be delegated as low down the component tree as possible, rather than hoisted up to the highest common parent.
- Fast Performance - we want to decrease JS bundle size, avoid client-server roundtrips and network waterfalls. If components are not dynamic, we should render them once on the server (and fetch their data), and not send their JS to the client. We should cache results on the client if possible. Requests that can be done in parallel should not be done in sequence.
- Historically, React forced you to choose 2 out of 3 of these constraints. With React Server Components, you can have all 3. (Facebook already had all 3 with Relay and GraphQL, but not everyone has a GraphQL backend, nor should they. React Server Components are a more general and easily adopted alternative.)
- 12:05 Lauren introduces the React Server Components Demo
- Server Components are indicated by the new
.server.js
extension (separate Module Conventions RFC here). - Client Components are also indicated by a
.client.js
extension. You can switch Client Components to Server Components simply by renaming them. - (Explained later) Shared Components just have the standard
.js
extension. - (Explained later) There are three new React I/O libraries used in the demo:
- react-fetch (wraps the fetch API)
- react-pg (to access PostgreSQL)
- react-fs (to access the Filesystem)
- These are caches that let you Suspend rendering while data is being requested.
- Fetching data on the server is much faster than fetching from the client, so this is a good optimization
- Server Components are indicated by the new
- 16:10 Server Components have zero bundle size impact
- React doesn't download any JS for Server Components, including for any of their dependencies. This is a great strategy for heavy dependencies that are only used on the server.
- (Explained later) JSX props/children will be rendered by the Server Component before it reaches the Client. So you can pass them Shared Components and save on bundle size too.
- 17:25 Limitation: Server Components cannot be interactive
- No
useState
oruseEffect
, no event handlers (e.g.onClick
) - Client Components must receive serializable props from Server Components (e.g. no functions. JSX children/props is fine)
- The RFC has more on the limitations of Server and Client Components.
- No
- 22:50 Difference between SSR and Server Components
- You can refetch the Server Component tree so that HTML updates stream down, without any extra JS for the refetched Server Components.
- But the parts of the app with clientside state are preserved. This is the main difference!
- Lauren later elaborated: "SSR of client JS apps is an illusion. You render your JS on the server into HTML. You serve that HTML to your client so it appears to have fast startup. But you still have to wait for your JS to reach the user before anything can be interactive (hydration). After hydration, SSR can't be used again - it's typically only used for initial loads. We can refetch React Server Components as many times as we like."
- The RFC FAQ also comments on the differences and complementarity of SSR and Server Components.
- 26:45 Shared Components
- Shared Components can render on either the server OR the client. They just have a
.js
extension. - Shared Components have the combined limitations of Server and Client Components, explained in the RFC.
- Shared Components rendered on the server won't be sent to the client, unless they are rendered on the client, in which case they will be downloaded on demand!
- Shared Components can render on either the server OR the client. They just have a
- 30:26 Server Components can directly query the database
- Because we can refetch the Server Component tree (as explained above)...
- a single Server Component that takes dynamic props and runs a database query (like a Search component) can update live!
- No local state management or API requests needed. Just changing props and refetching the Server Component.
- This Component stays updated even as you add new items to the database!
- Don't skim over this part - This is a literal WOW moment - see us realize this at the 1h 10min mark on the React Serverless Components demo livestream
- 33:21 Suspense and Slow Network UX
- When the network is slow, we should show some immediate feedback to users as they wait. Suspense and Server Components work well together for this.
- Method 1: We can use Suspense fallbacks to show user some feedback on Server Components, even while waiting for the rest of the server tree to be rendered (eg due to fetching data)
- This is possible because Server Components don't render to HTML, they render to a special format to be streamed down to clients.
- Method 2: We can also use Suspense transitions to respond immediately even before the response has started streaming down.
- 36:50 Recap of Demo Points. Server Components...
- 36:54 have ZERO effect on bundle size
- having the ability to decide which components to put on the client or on the server gives you more control over bundle size
- 37:42 let you access backend resources DIRECTLY
- You can use the same paradigm for server & client data fetching
- The community will be able to create more React IO library wrappers to cache results for more data sources
- 41:04 let you only load the code that is necessary
- if a Server Component conditionally renders a Client Component, the server will only send an instruction to download the Client Component if it is present in the server output
- This works like dynamic imports, but is automatically done instead of requiring manual loading
- To enable this, Integrated plugins are being written with webpack, Next.js, and Parcel teams.
- 43:17 let you decide the tradeoff for every concrete usecase
- Eliminate the artificial boundary between client and server:
- Put data fetching and preprocessing code on Server
- Put fast interaction response code on Client
- But they expect that the majority of components will be Shared.
- Example: CMS where you render articles on the server (so can be Server component most of the time) but when in admin panel you want to edit them (so need to render on client)
- Eliminate the artificial boundary between client and server:
- 44:25 provide modern UX with server-driven mental model
- create modern and app-like UI's
- but write them like an old-school web page
- Example: the Search component in Lauren's demo is a Server Component that refetches based on a Client Component's state, but that Client Component's state persists
- Usecase: CSS animation on property change can fire because now the DOM will not be destroyed
- 36:54 have ZERO effect on bundle size
- 47:14 Recap of Recap & looking ahead
- It's Opt-in, still in R&D (missing core APIs eg Server Component Router, and need to finalize Concurrent Mode), and will be available to everyone even those who don't use Next.js
- Production tests at FB have seen up to 29% reduction in bundle sizes
- Don't make courses on this or put this in production. Just play with the demo.
You can also see more commentary on the Twitter thread here.
Demo Walkthrough
The React Team also released the demo shown in the talk: https://github.com/reactjs/server-components-demo/
However there are some difficult setup steps and a lot of things to try in the demo. I recently did a 2 hour walkthrough of every part, with timestamps.
You can also put Server Components into a serverless function, as I found today with a lot of difficulty.
Personal Reflections
This is the beginning of the end of a very, very long journey in React stretching as far back as 2014, when Jordan Walke first mentioned Async Rendering (the old name for Concurrent React) was a possibility.
My relationship with React Suspense started just after Dan Abramov's JSConf Iceland 2018 talk introducing it. It blew everyone's mind and ripped up everything I thought I knew about how React apps could and should be written. I stayed up all night to write a walkthrough of that demo. I had no React presence at the time but I've been following this idea throughout the ensuing 2.5 years.
I suspect how React Server Components look to people who have followed the journey are wholly different to people who are only just looking at this today. Kind of like how you might really enjoy a movie just for your snobby annoying friend to tell you "the book was better".
To me, "the book" provided a whole lot more context that, while it is irrelevant today, gives me a great deal more appreciation for how we got here and how things might work under the hood.
Here is all the jargon we did NOT discuss at any point in the talk or demo (because they are implementation details or irrelevant today):
maxDuration
and Just Noticeable Difference heuristics- Priority Lanes
hidden={true}
- Progressive Hydration
- Selective Hydration
- Scheduling
- Transparent Asynchrony
- Idempotency vs Purity
- Coroutines
- Concurrency, Algebraic Effects, Lazy Eval
- Two pass SSR
- Streaming SSR
- Trisomorphic Rendering
- Islands Architecture
Instead, we are given one concept today - Server Components. If you squint hard enough, you might even observe there is no API.
Takeaways
Because the React team has consistently communicated their thoughts through the years, the main benefit of React Server Components is the same as I've commented on in my writing and speaking.
Given a baseline React runtime, React Server Components let you:
- Greatly reduce real-world app sizes by making it easy for you to not send down unnecessary JS (the proverbial disappearing app)
- Have Client and Server components seamlessly operate in the same tree so you don't have to change paradigms when requirements change.
- Serve data much faster and easier by skipping client-server waterfalls and directly accessing backend resources.
- Preserve clientside state and DOM nodes even when loading Server Components so you can even do page transitions, CSS animations and persist input focus.
This is a wonderful new area of opportunity for React developers, and the ecosystem is set to see tremendous growth in 2021. I feel certain that it is also going to spark similar efforts in other frameworks (because the case for Concurrent Mode has for the first time been made beyond time slicing and lazy loading components).
Related Reads
I will update this with selected other pieces over time.
- Dan's comment about an unmentioned Instant Transitions feature
- Dan's responses to RFC comments
- Addy Osmani's take on React Server Components
- https://hotwire.dev DHH's bundling of the Turbolinks + Stimulus + Strada stack used in the Hey email app. RSCs have been (loosely!) compared to "Turbolinks for Virtual DOM"
Join 2,000+ developers getting updates ✉️
Too soon! Show me what I'm signing up for!