React Hydration

29. July, 2022 5 min read Develop

Understanding React Hydration

Let's initially load a scaffolding HTML representing our React application, followed by hydrating it with functionality to improve user experience and SEO!

That is the gist behind React Hydration. First, instead of showing an empty page on load, we leverage ReactDOMServer’s capabilities to render our components into the DOM during server-side rendering (SSR). Then, during client-side rendering (CSR), React Hydration kicks in and gradually adds interactivity to our components.

That premise seems simple, but the approach can cause performance concerns and even serious issues. This article will deal with them and provides additional references to strategies.

How does it work?

Let’s have a look at a basic React app and how it is created:

<!-- index.html -->
<html>
  <head></head>
  <body>
    <div id="root"></div>
  </body>
</html>
// DemoApp.js (component)
import React from 'react';

function DemoApp(props) {
  return (<div>Hello {props.title}</div>)
}

export default DemoApp;
// index.js (CSR)
import React from 'react';
import ReactDOM from 'react-dom';
import DemoApp from './DemoApp';

const root = ReactDOM.createRoot(
  document.getElementById('root')
);
root.render(
  <React.StrictMode>
    <DemoApp title="React Hydration" />
  </React.StrictMode>
);

In this scenario, the website will display an empty page and fill it with our demo application’s “Hello React Hydration” text. This process can be a problem, as the content “flashes” into existence, and crawlers don’t necessarily know about your content.

To remedy this, we can use SSR to serve our components to the browser without any interactivity:

// server.js (SSR)
import React from "react";
import ReactDOMServer from "react-dom/server";
import DemoApp from './DemoApp';

app.use('*', (request, response) => {
  fs.readFile(
    path.resolve('./index.html'), 'utf-8', () => {
    return res.send(ReactDOMServer.renderToString(
      <DemoApp title="React Hydration" />
    ))
  });
});

React Hydration will then gradually add that interactivity by using hydrateRoot instead of createRoot during CSR.

In React < 18, it would be render and hydrate instead of createRoot and hydrateRoot.

// index.js (CSR)
import React from 'react';
import ReactDOM from 'react-dom';
import DemoApp from './DemoApp';

const root = ReactDOM.hydrateRoot(
  document.getElementById('root')
);
root.render(
  <React.StrictMode>
    <DemoApp title="React Hydration" />
  </React.StrictMode>
);

This technique is how React Hydration is applied to an application. Render a pre-build version of your application on the server, and then add interactivity through the hydration process.

This approach is also used by many static site generators such as Gatsby or Next.js. The sites got pre-generated by SSR and served via their public/ folders. The browser can “pick up” where the server left off and add app-like features accordingly.

However, this approach requires the React tree to be in-sync with the DOM. Otherwise, we end up with issues.

Issues with Server-Side Rendering and Hydration

React Hydration requires our DOM to be in sync with the React tree. Through SSR we pre-fill the DOM with the markup of our application, and React Hydration picks it up from there. What happens if our SSR DemoApp serves different content depending on SSR/CSR generation? Let’s have a look at a typical example:

// DemoApp.js (component)
import React from 'react';

function DemoApp(props) {
  const greeting = typeof window !== 'undefined'
   ? 'Hello ' : '';

  return (<div>{greeting} {props.title}</div>)
}

export default DemoApp;

In this case, if the window is defined, our app returns “Hello React Hydration” and, if not ”, React Hydration”. Now we end up with a mismatch, and the following error is thrown:

While rendering your application, there was a difference between the React tree that was pre-rendered (SSR/CSR) and the React tree that was rendered during the first render in the browser.

This issue is widespread and must be dealt with when working on SSR. Not all features are available on a server compared to a browser. For Hydration to work, the tree needs to be identical.

The above example could be avoided using useEffect, as the method is only called during the hydration phase and not while pre-rendering the content. For example:

// DemoApp.js (component)
import React, { useEffect } from 'react';

function DemoApp(props) {
  // be empty during pre-generation
  const [greeting, setGreeting] = useEffect('');
  // add "Hello" after hydration takes place
  useEffect(() => setGreeting('Hello '));
  return (<div>{greeting} {props.title}</div>)
}

export default DemoApp;

This change causes the content to have the “flashing” effect but can be softened through loaders or skeletons.

There are other issues to consider, for example, when pre-fetching APIs during the SSR process. They need to be appropriately cached or passed onward to avoid mismatches. It is also a pervasive process when working with Gatsby or Next.js.

Hydration strategies

The idea behind different strategies is to decide which components need to be hydrated. Your website becomes unresponsive during the hydration process, which can block your users from interacting with your product. It might not be a big concern for smaller websites, but when you load huge bundles to add interactivity, it can get frustrating to wait to click on a menu to move onwards.

Different approaches surfaced in the past years to solve these problems. The strategy shown in this article uses Streaming Server-Side Rendering which has been available since React 16’s ReactDOMServer.renderToString API.

Another is Progressive Hydration or Hydration on demand where you attach specific criteria to your components. These can be on visibility, scrolling, idling, or with a simple delay.

And the last I want to cover is Selective Hydration which uses pipeToNodeStream instead of renderToString. This method makes it possible to start streaming HTML without waiting for the more significant components to be ready. Meaning that you can lazy-load components when using SSR.

There is also a great library collection from Google Chrome Labs on the different approaches if you are interested in code examples.

Let’s hydrate some more 💧

React 18 fixes several issues people often encounter when using SSR with React and Hydration. We’ll look closely at React 18’s changes in the following article.

‘Till next time!