It's been a couple of years since the React team started conversations around React Server Components (RSC) and with React 19 on the way, it's a good time to become familiar with this new paradigm of building React applications.
New paradigm?!??? Yes, indeed a new paradigm! Ever since the request for comment for RSCs, there has been a lot of confusion as to what react server components actually are. In short, RSCs are a way to render react components exclusively on the server. But what does that mean? How does it work? What are the benefits? And how do RSCs fit together with Server Side Rendering?
I've spent the last year experimenting with RSCs, both on the concept level and on the framework level and I wanted to share a piece of what I've learnt. In my opinion, RSCs are truly something to be excited about as they enable new user experiences while also improving the overall performance of an application! They are soooooo cool!!
Throughout this article we'll deep dive into what React Server components mean, we'll answer the questions we posed above, and explore some future possibilities as a result of moving an application to RSCs model.
A step back
So that we can all gain an appreciation for React Server Components, I think it would be helpful to take a step back and speak on a few common rendering strategies when it comes to thinking about React.
Client-side rendering
When using tools such as vite to scaffold a react application, most tutorials speak of a client rendering strategy. With this strategy, the user will initially receive an HTML document that looks like the following:
You'll notice that all the content is missing!! That's because the main.js includes all the code we need to mount and run our application -- this includes React, third-party dependencies, all the UI we've written using React and any other piece of code.
Once the JavaScript has been downloaded and parsed, React will come to life, creating all the DOM nodes necessary for our application while attaching any event handlers as well. All these nodes become a child of that singular div element within our HTML document*. This means that with a client rendering strategy, our application will not become visible to the user until all the JavaScript has been downloaded, and parsed, and React has created the DOM nodes. We can see this demonstrated in our example below. Click the button to view a client rendering strategy!
HTML
CSS
JavaScript
This is a problem because the user will be staring at a blank screen until all of the above processes (HTML, CSS, and JavaScript) have finished. This problem also tends to get worse as the JavaScript bundle gets larger and larger.
There are optimizations to help mitigate the prolonged waiting period; optimizations like lazy loading, loading spinners, etc but these optimizations do not help tackle the main problem at hand. How do we render elements to the screen quickly with a growing bundle size?
Server-side Rendering
Server-side rendering is a rendering strategy that is aimed at improving this experience. Instead of sending an empty HTML file (detailed in the client-side rendering section), the server will generate an HTML document which includes all the elements required for the initial render. We are effectively rendering web pages on the server before sending them to the client. As a result, the user no longer sees a blank screen on the initial render but rather they will see some information about the page. This rendering strategy also helps search engines crawl and index the initial content on the page which is beneficial for Search Engine Optimization.
Consider the following example! Click the Render Components button and see what happens!
HTML
CSS
JavaScript
How does this differ from a client rendering strategy?
We can see that after the HTML is downloaded, we get some content being displayed to us. More specifically, we see that the HTML page being sent from the server contains all the elements needed to render an initial page. Using a server rendering strategy we don't have to wait for the javascript to the downloaded, parsed, and react to spring into action before we see some sort of content.
It's important to note that the HTML file sent to the client on the initial render still includes the script tag detailed in the client rendering section as the browser still needs React to run on the client. However, things work a bit differently after the JavaScript bundle has been downloaded. Instead of building all the DOM nodes from scratch, React will use the initial HTML document as a scaffold to hydrate the DOM nodes. We use hydrate here as a way to refer to the process where React takes over rendering and attaches all event listeners registered on a JSX element.
Therefore, a server-side rendering strategy improves a user's experience by showing some content to the user on initial render without having the user wait for the JavaScript to be downloaded, parsed, and react to spring into action. The user sees some content while all the other stuff is happening in the background and then when all is complete Client-side React will pick up and now we get some interactivity on our page.
SSR is a generic term
Server-side rendering is a generic term that encompasses much more than we described above. When we mostly think of server-side rendering, I imagine it looks like the following:
Request is made for the page
Server generates an initial HTML document and then sends it to the client
The user sees some content on the initial render
This is one way to implement server-side rendering and it's typically referred to as server-side rendering at "request" time *. The other type of server-side rendering is done at "build" time; often referred to as static site generation (SSG) *. During the bundling of our application at "build" time, we slot in an extra step which allows us to "pre-render" all the HTML for all our different routes.
Essentially we generate static HTML files for all the different routes that are available and then push/store these files on a content delivery network (CDN).
Show more
Data fetching
We've spoken about two major rendering strategies an application can have when it comes to displaying content but for us to fully gain an appreciation for React Server Components, we must also talk about data fetching.
Many of us have been working with React from the client-side rendering perspective using scaffolding tools like vite. We previously mentioned that a client rendering strategy involves the user receiving a blank HTML file which is then populated with content after all the JavaScript has been downloaded, parsed, and React has sprung into action. This means that data-fetching within client-side rendered applications can only begin after React has sprung into action.
We can see can see it visualized here:
This strategy is where you most often see loading spinners to indicate that the content is still waiting to be rendered on screen*. React developers may be familiar with the idea of setting a isLoading state variable which is used to conditionally render loading spinners until the content is available.
Although CSR is a viable rendering strategy when it comes to data-fetching, there are many situations in which we would want the initial rendered shell to contain some content instead of being blank. As we mentioned above, this goal SSR aims to achieve -- we render an initial shell with some content, download JavaScript and then run react to make our db query and get the rest of the content.
We can see it visualized here:
Looking at these two images above, we can say that CSR and SSR are very similar as both approaches download JavaScript on the client and then make an additional request to get the remaining content to be rendered on the client. The only difference is where the initial render is taking place -- with CSR this occurs on the client after JS is downloaded on the client whilst with SSR the initial render occurs on the server before the JS is downloaded on the client.
With frameworks like NextJS -- specifically the /pages directory -- we could move more of the database querying actions to the server. This was accomplished by exporting one of two functions that would exclusively run on the server from the same file as the component*. The two functions were getServerSideProps and getStaticProps. If you're familiar with the /pages directory of NextJS then the following code may be familiar to you.
These functions were cool because they returned a props object containing the data needed for the component to render with the full content on the initial render. This was an improvement but even with SSR or these modified approaches we were still rendering react components on both the client and the server.
But what if we could exclusively render react components on the server? What would that look like? What could be made possible?
React Server Components
React server components (RSC) are a way to render react components exclusively on the server. These components allow us to write code that seems questionable but magically works!!
Let's move the code snippet from the NextJS /pages example into a React Server component to give us a visual representation of what they look like and then we'll talk about it!
WAIT WHAT?!???? React Components can be marked as async? We can run db queries directly inside react components? Isn't that a HUGE security risk? How are these things possible?
Server components * are just the regular semantic react components that we've been writing all along. They are constructed from a function that can define some props and then return a piece of jsx to render a view. The difference here is that server components are rendered exclusively on the server with the rendered value * being sent to the client to be displayed. As a result, code written inside RSCs is excluded from the JavaScript bundle thereby allowing us to access server-side data sources * directly inside a component.
The rendered value RSCs produce
A misnomer about RSCs is that the server exclusively renders a react component and then sends down a finalized HTML string to the client. This idea is partly true in that the server does exclusively render a react component but a finalized HTML string does not get sent down to the client in the RSC model.
In actuality, the server renders the react component and then sends an RSC payload down to the client. This RSC payload is serialized JSON that describes to the client how the react component should be built. In other words, the RSC payload is a virtual representation of the component which was rendered on the server.
It looks something like this (simplified version):
Show more
Fantastic!! But we've run into a problem though.
RSCs never re-render as they run once on the server to generate the UI and then send the result to the client to be displayed. This means that most of the React hooks we've come to love are now incompatible with RSCs. We can't use useState because useState causes a component to re-render but server components can't be re-rendered.
But I thought the entire point of React was to have interactivity within our components. If we're using RSCs, how do we inject interactivity into our applications?
Client components
Client components solve this issue!! What?!???? More new terminology???
Client components are nothing new as they are the "standard react components" that we've been writing all along. The term is used to distinguish between server components -- the new type of component -- and the "standard" react component. This means that client components allow us to access all the react hooks that we're familiar with!! We can use states, effects, browser-only APIs, etc.
However, the term client component is misleading! I wanted to emphasize the point of the "standard" react component because we would assume that client components are only rendered on the client. However, that is not the case as client components are rendered both on the client and the server.
Misnomer of RSCs
A misconception of React server components is that they are a replacement for server-side rendering. It's a fair assumption to make and I'm guilty of this too haha!! They do sound fairly similar but React Server Components are not a replacement for Server Side Rendering. These paradigms can work hand-in-hand or independently of each other.
As we saw above, RSCs are a way to render a component ahead of time in an environment that is separate from a client app while SSR is a rendering strategy that generates an HTML document which includes all the elements required for the initial render.
Defining client & server components
WOWWW!! We have a server component (a new type of component) and a client component (a new name for a familiar type of component). So the question remains, how do we go about defining these components when building a react application?
During the initial conversations of RSCs, the thinking was that we would mark a client component with a .client.js extension and a server component with a .server.js extension (similar to how it's done in Remix). However, these conventions evolved and we rethought how we specify client & server components.
The new thinking (the one that stuck) is that all components are assumed to be Server Components by default. We have to “opt-in” for Client Components. We do this by specifying a new directive use client.
We use the use client directive to mark a file/component as a client component thus all the code in this file will be included within the JavaScript bundle.
When we mark a file with the use client directive we are creating a client boundary between client and server. Therefore, every component past this boundary will implicitly become a client component.
We can see this demonstrated in the following component tree:
Pop Quiz
Any component imported into a file marked with "use client" will also become a client component
Looking at the diagram above, it makes sense that we can render client components inside of server components because once the server bundler hits a client boundary, it marks that location and includes the appropriate JavaScript code in the client bundle.
Interesting!! But can we render a server component inside of a client component? We said that once we create a client boundary, we implicitly convert every component within the boundary into a client component. So is it possible?
Technically, no but also yes! We can still render server components inside of client components if and only if we pass them as props (ie children props). The important thing to note here is that any component that we import into a file marked with use client will also become a client component as that file/component is now inside the client boundary.
Why use RSCs?
So far all the text you’ve been reading thus far, including all the code blocks, are actually React server components!
That’s right! These components are not interactive so there’s no need to ship the code to as part of the client bundle. We can do the work on the server and then just send down the rendered value for the client to display. Furthermore, syntax highlighters tend to have very large bundle sizes so to reduce the overall size of the client bundle you receive, it makes sense to render the code blocks on the server and then just send down the rendered values to the client.
Components such as the pop quiz you just took, need to be interactive. Therefore, these components are marked as client components so it means that the JavaScript needed to render these components is included in the client bundle that you receive.
This interweaving of server components with client components coupled with the ability to define our client boundaries (opt-in system) offers many advantages and new ways of structuring our application which we will come to appreciate later on.
The Story of Server Actions
So far we've been concentrated on defining react server components and how they differ from the newly termed client component. However, we've looked at only one part of the puzzle -- reading data and rendering components in a server-only context. What happens when we mutate data? Do we still have to do this on the client? Is there a way to mutate data exclusively within a server context as well?
This is where server actions come into play! Server actions provide a way to run functions/logic exclusively within a server context. This means that mutating data within a database is now as simple as calling a function within your component!
This is awesome because we no longer have to rely on building API routes and hooks to handle a form submission! We can literally just call a function within our component!
Let's take a look at what a server action looks like
We can appreciate that server actions are just functions at the end of the day but they differ from regular async functions in that they are marked with the use server directive at the top of the function *.
We can use server actions as follows:
Above we see that we have a react component that is rendering out a form element. You'll notice that we are not providing an onSubmit handler to handle form submissions but rather we are using an action attribute.
Here the action attribute is used to invoke the server action which creates a POST endpoint behind the scenes and then executes the function within a server context. We can have multiple actions on the same page and React will keep track of what actions correspond to what form submissions *. Server actions can also be executed without the need for a form as we can use the useTransition hook to trigger these actions.
Are server actions only for server components?
It's a misnomer that server actions can only be triggered within server
components. In actuality, we can use server actions within client components
by just importing them and passing them to the form action attribute, or the
useTransition hook.
It's important to note that actions don't just exist on the server because we can also establish them on the client side but that's a story for another time.
The little big things
There are a host of questions that arise with the new implementation of React server components and server actions. If we are no longer relying on manually calling an API endpoint, how can we show pending states, rejected states, etc? What benefits arise when we move over to the server action paradigm? Lastly, regarding server components, what does it mean to stream content from the server and what are its benefits?
Handling pending and rejected states
Several new hooks in react come with the implementation of actions and one of them is the useFormStatus hook. useFormStatus gives us access to the status information of the last form submission. Using this hook, a component can know if its parent's form (using actions) has been submitted and can respond accordingly. For example, if we want to disable the submit button to prevent multiple submissions from occurring then the useFormStatus hook can give the component the necessary information to accomplish that task.
useFormStatus can only be used in a specific way
useFormStatus returns the status for a specific form element, so for the
hook to work correctly, it must be defined inside a component that is a
child of the form element. It's also a React hook thus it can only be used
within a client component.
There is also the useActionState hook which allows us to update the state of a component based on the results of a form action. In this case, if we return from a server action, we can access the return value and use it within the component.
Consider the following server action, useFormStatus, and useFormState example!
Here we can see that the result returned from the server action can be accessed within the component and displayed accordingly. We can also see that we are disabling the login button depending on the status of the form. This syntax drastically improves the developer experience because features that we would have written excess code for can now be accomplished in just a few lines of code!
Optimistic Updates and UIs
The benefits don't just stop at the developer experience! With this new way of thinking about submitting user inputs, we can begin to build UIs that support optimistic updates. These experiences have been available on mobile platforms for a while now but it's so cool that we can begin to implement on the web platform.
If you're not familiar with optimistic updates/UIs then TL;DR -- optimistic updates allow us to immediately present the user with the intended result after they have taken an action. We optimically show this result even though the action may take some time to complete.
Take a look at the chat component below to see what I mean:
React
# Thread
Hey, are you coming to the meeting today?
We're going to be discussing Optimistic UI elements and how we can add them to our React app.
Don't forget to bring your questions!
When you send a new message, you immediately see the updated changes with the expected outcome instead of waiting for the server's response. But once the server finishes processing the request, the UI will be rerendered in order to show the correct state. This makes the app feel more responsive and your user is not kept waiting for a long period if they have a slow connection.
In React, we can accomplish optimistic updates using the new useOptimistichook which allows us to show an optimistic state change when an asynchronous action takes place. In the case that an error is thrown, the state will fall back to the previous value before the action took place.
This provides a great experience for the user as they get the sense of rapid behaviours they experience but with the added experience that error will be handled gracefully.
Streaming
The last point we'll touch on here is the idea of streaming content to our users. When we think of streaming we may think of watching a video on our favourite streaming platform. There we may notice that pieces of content are being downloaded to our devices as we are consuming the content. This is in contrast to the other method of first downloading the entire video before we can watch it. The thinking here is that if we can break up the video into little chunks and then send those chunks to the user as they need, then we can speed up the time to interactive for the given video. This gives the perceived performance improvement for the user and they get to watch the video "faster".
Toggle between the without streaming & with streaming tabs and click on the Render Components button to get a sense of what streaming looks like in action!
We can see that in the without streaming scenario, we have to wait until the slow component finishes rendering before we see any sort of content. In the with streaming scenario, we can get the fast components immediately and then show the slow component once it's available.
Using React, we can accomplish the same experience using a fairly new component. The suspense component coupled with react server components allows us to introduce streaming directly within our applications. We can directly render high-priority components and then stream lower-priority components to our users. This way we can reduce our Time to First Byte, and improve our Time to Interactive, thereby allowing users to see and interact with important information more quickly.
Wrapping up
React Server components bring about a new paradigm of building React applications. It's a whole new way of thinking about and writing react apps!
We've explored the what, the who, the why, and the how of React server components and we've also looked at some of the benefits RSCs enable. We revealed that RSCs are a way to render react components exclusively on the server and it could be done on-demand (coupled with SSR) or be part of the bundler at build time. We showed how the server-only context could enable us to make DB queries alongside other server-only queries directly within our component. We revealed that server components differ heavily from server-side rendering as RSCs send a serialized payload describing how a React component should be built on the client.
Throughout the article, we've constantly reinforced the idea of the server-only context of RSCs. We said that code written inside RSCs is excluded from the JavaScript bundle thereby allowing us to access server-side data sources. We never explicitly said it but this means that RSCs have a lot to do with the bundling process. To get to a stage where we can only run code within a server-only context, the server must decide on what to include in the final JavaScript bundle that is sent to the client. This means that to properly integrate RSCs into an application we must take a top-down approach and start on the server.
The topmost component of an application using RSCs (in the diagram BlogPage) will always be a server component. Only then can we decide what children components below are going to be client components. This comes in very handy when we start to think about refactoring our applications to use RSCs.
So where does that leave us?
We talked about some of the implications of moving to a React server component and server action architecture. We said that we gain new abilities such as streaming and optimistic updates which ultimately lead to a better user experience. But in doing so we said that we had to rethink how we approach handling form submissions. The new way of interacting with form submissions brings back the idea of progressive enhancement and allows us to optimize our application while still allowing us to create custom experiences.
WOWWWW! What a journey!! React server components are truly a new paradigm for building applications! They are SCARY because RSCs make us rethink how we should go about building react apps but they also encourage interesting patterns that improve the overall developer and user experience. It's definitely going to be interesting to see what new patterns emerge!!
There's so much more to discover with this new paradigm and it's going to take a lot of getting used to! I'm going to leave you with a few questions to ponder: What happens to the separation of concerns? How does the definition of frontend and backend change? Do react components reclaim this idea of truly being Lego pieces?
All right, I'm going to wrap it up there! Hope you found this useful, and I'll catch you in the next one... Peace!
Practice problems
PSSSST! Hey you! Yaa you! Enjoyed the article?? Here's a fun little exercise for you to try out! 👀
Exercise
These are a series of questions or mini-games that are associated with the article you just finished reading! They are meant to help solidify the concepts talked about in this article. Have fun!
export async function getUser(prevState, formData) { //... some logic goes here and an error is thrown return { message: "Please enter a valid email address", };}
const App = ({ data }) => { return <p>The name of the blog is {data.title}</p>;};export default App;export const getServerSideProps = async () => { const data = await sql`SELECT * FROM Books`; return { props: { title: data } };};
const App = async () => { const data = await sql`SELECT * FROM Books`; return <p>The name of the blog is {data.title}</p>;};export default App;