πŸ’§ Hydration and Server-side Rendering

Featured image created from photos by Dan Gold and insung yoon on Unsplash.

This is part 8 of my series on server-side rendering (SSR):

  1. πŸ€·πŸ»β€β™‚οΈ What is server-side rendering (SSR)?
  2. ✨ Creating A React App
  3. 🎨 Architecting a privacy-aware render server
  4. πŸ— Creating An Express Server
  5. πŸ–₯ Our first server-side render
  6. πŸ– Combining React Client and Render Server for SSR
  7. ⚑️ Static Router, Static Assets, Serving A Server-side Rendered Site
  8. [You are here] πŸ’§ Hydration and Server-side Rendering
  9. 🦟 Debugging and fixing hydration issues
  10. πŸ›‘ React Hydration Error Indicator
  11. πŸ§‘πŸΎβ€πŸŽ¨ Render Gateway: A Multi-use Render Server

Over the course of this series on server-side rendering (SSR), we have tackled some interesting problems1.

In the last post on this topic, we finally combined our client app and server, giving an SSR result that looks and works a lot like the non-SSR version…or at least it seemed that way. As it turns out, we are missing a big piece of the SSR story and, I'm afraid to say, that once we add that in we will see that we still are not quite done. That piece is hydration.

ℹ️Code for this series can be found at https://github.com/somewhatabstract/hello-react-world.

πŸ™‹πŸ»β€β™‚οΈ What is Hydration?

React does a lot of things to help simplify the rendering and operation of a client-side web application. In order to do that, a root component that represents our application has to be mounted into the page. This is done via the ReactDOM.render call (React 18 and up use ReactDOM.createRoot). It takes the component representing our application and a DOM node indicating where in the page the application will exist. In our simple web application, this looks something like this.

ReactDOM.render(<App />, document.getElementById('root'));

Without server-side rendering, the root node that we are giving here is entirely empty. When the ReactDOM.render call occurs, React fills that empty node with our application. When we have an SSR result, that node already has content in it. Instead of replacing that content, we want React to attach the running application to it. This is where hydration comes in. During hydration, React works quickly in a virtual DOM to match up the existing content with what the application renders, saving time from manipulating the DOM unnecessarily. It is this hydration that makes SSR worthwhile.

There are two big rules to hydrating an application in React.

  1. The initial render cycle of the application must result in the same markup, whether run on the client or the server.
  2. We must call ReactDOM.hydrate (React 18 and up use ReactDOM.hydrateRoot) instead of ReactDOM.render in order to instruct React to hydrate from our SSR result.

We will get back to the first item as it is going to drive some further work once we have addressed the second; making React hydrate our SSR result.

🚰 Making our application hydrate

There are two ways we can make our page hydrate. We could either assume we will always SSR and therefore always call hydrate instead of render, or we could support both hydrate and render depending on how our application gets deployed. The benefit of the former is that we do not have to do any branching in our client-side code for the two modes; the benefit of the latter is that our application is flexible, allowing us to use the client-side application without demanding that we SSR, even in development.

In the end, it really is best to support both, so we are going to need to update our client application to understand the difference between the two states and act accordingly. So, how does our client application know which to do; hydrate or render?

There are many ways to do this. Some things that spring to mind are:

  1. Look at the element being used for the root of our application and if it has children, hydrate instead of render.
  2. Embed JavaScript in the page markup that sets a flag to specify one or the other.
  3. Use a function to perform the action and then use embedded JavaScript to change which operation that function performs.

I am sure you could think of some more. Since we already have to get the element in order to mount the React application, I am going to stick with option 1 for now (we can always change our minds if need be). Below I have included the index.js of our client application before and after adding hydrate support. In both files I have highlighted what changes. Note that we do not even have to touch our SSR implementation in order to get this working.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

const mountElement = document.getElementById('root');
const reactMountFn = (mountElement.childElementCount === 0)
    ? ReactDOM.render
    : ReactDOM.hydrate;

reactMountFn(<App />, mountElement);

Right, let's see what happens when we run our application with this updated code. First, in development mode, we start the client and the server2. If we have done this correctly, visiting the non-SSR route should use render and visiting the SSR route should use hydrate; and if we've really done things correctly, we'll never know.

Developer Mode render
Developer Mode hydrate with SSR

Oh dear; something is not looking so great. It feels like we took a step backwards here. Last time we had a server-side rendered application that looked just like the client-only version, and now we're back to not even loading the SVG asset properly. What happened?

🚱 When hydration fails

If we open the console for the version that is performing hydration, we can see a big hint to our problem.

Error in Google Chrome console reading:

Warning: Prop `src` did not match. Server: "/Users/jeffyates/git/hello-react-world/client/src/logo.svg" Client: "/static/media/logo.5d5d9eef.svg"

I had mentioned at the end of our last post that our SVG was getting the wrong path because we were not including our root React component with all the webpack magic, and this is the result. Our server-side rendered content has a different path than the React application wants to use when rendered on the client.

Did I say rendered? Don't I mean hydrated? Well, React is rendering the client application in a virtual DOM and comparing it to the content it is trying to hydrate in the page (our SSR result); if there is a mismatch, the rendered part wins. In the worst case, this can cause the entire application to re-render in the client in order for React to guarantee the client-side application is in the correct state.

These hydration warnings are, in my view, a big deal but are so easily overlooked. They are hidden away in the console messages of development builds – production builds do not raise them (React 18 and up will surface hydration errors in production), React just silently deals with the situation in the way it thinks best, which could be a total re-render of your application. Not only that, but they are pretty hard to debug. This one is clear, but when it comes to others, they may just say that there's a div it did not expect; good luck finding out which div that is. Finally, if there is more than one, you will only see the second one once you fix the first, so staying on top of these is important if you want to ensure your SSR results can be used to their full potential, avoiding those costly client-side re-renders if the hydration fails.

πŸ€— Don't Panic

Of course, there's no need to panic; we have made incredible progress. We actually have a server-side rendering application that is attempting to hydrate. This is pretty fantastic and the only error we seem to have right now, the SVG path mismatch, is as much down to how our code is built using webpack as it is to do with React. Next time, we will dig into this hydration warning, work out how to fix our SVG path, and pave the way for a deeper dive into fixing hydration issues.

As always, thank you for your attention. Server-side rendering is a fun topic, in my opinion, but it can seem overwhelmingly complex3. I hope this series is proving useful in breaking apart that complexity and surfacing the sharp edges on which we can get easily caught. Let's smooth those edges down together.

Until then, be well! πŸ’™

  1. I think they are interesting – I hope you agree []
  2. Remember, last time we made it so that our SSR server gets files from the client dev server to ensure we get that good webpack magic. []
  3. it was for me when I first got started []

9 thoughts on “πŸ’§ Hydration and Server-side Rendering”

  1. Loved the series! It gave me some great insights into how SSR works that I couldn't find elsewhere, especially this explanation about hydration vs rendering!

    1. Good point. I'll make an edit to correct the post. Thank you for the correction.

      PS I think you might be heavily misusing "heavily misusing" 😏

  2. Hey Jeff, just wanted to say thanks for the great series! I found it to be super helpful, more so than everything else I read on the topic. Kudos!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.