Understanding Hydration in React applications(SSR)

In the React world, we come across different terminologies like server-side rendering (SSR), client-side rendering (CSR), ReactDOM, ReactDOMServer etc., which we might not be fully aware of.

Let us quickly understand these terms.

ReactDOM

ReactDOM is a package that presents DOM-specific methods, which can be used at the topmost level of a web app. It facilitates an efficient way of managing the DOM components of the web page.

render() and hydrate() functions are the modules for the react-dom package.

render()

  ReactDOM.render(element, container[, callback])

The render() function is one of the most useful functions of ReactDOM. It returns a reference to the component after rendering a React element into the DOM in the provided container (or returns null for stateless components).

hydrate()

  ReactDOM.hydrate(element, container[, callback])

hydrate() is the same as render() but is used to hydrate a container whose HTML contents were rendered by ReactDOMServer. React will attempt to attach event listeners to the existing markup.

ReactDOMServer

The ReactDOMServer object enables us to render components to static markup.

renderToString() renderToStaticMarkup() functions of ReactDOMServer can be used in both the server and browser.

Client-side rendering (CSR)

Client-side rendering (CSR) means rendering pages directly in the browser using JavaScript. All logic, data fetching, templating, and routing are handled on the client-side.

When a browser receives a request for a page, it sends HTML, CSS and, JS code to be run in the browser. The script tag contains all the instructions in the React code. It is loaded in the browser, and the app becomes interactive.

CSR might take time to make the site visible to the users in case of slow internet or large bundle size. A user generally sees a blank page in such scenarios. It gives a bad user experience. It also affects SEO as web crawlers are unable to index the blank site.

Image is taken from a talk by Shaundai in React Conf 2021.

Server-side rendering (SSR)

To understand SSR let us take an analogy of food served in a restaurant. Suppose we placed an order of starter, chapati, dal, curry, etc. It takes a long time for the order to be delivered. We would be frustrated. A hungry customer might even scream at the waiter.

But, what if we are served some papad, salad, or a welcome drink as we take our seats?

Amazing! At least we got something to start with!

A similar approach is used in SSR.

In server-side rendering, when a user requests a webpage, the server prepares an HTML page by fetching user-specific data and sends it to the user’s machine. The browser then constructs the content and displays the page. This entire process of fetching data from the database, creating an HTML page and sending it to the client happens in a few milliseconds.

In this process, users can see the content on the browser instead of a blank screen, making the users happy and improving their user experience.

Image is taken from a talk by Shaundai in React Conf 2021.

Now, let us look into Hydration in React applications.

Hydration

React hydration is a technique used that is similar to rendering, but instead of having an empty DOM to render all of our react components into, we have a DOM that has already been built, with all our components rendered as HTML.

Basic React app:

const root = document.querySelector("#root");
ReactDOM.render(<App name="Saeloun" />, root);

The output of the application before loading scripts on the client-side:

<html>
  <head></head>
  <body>
    <div id="root"></div>
  </body>
</html>

SSR application:

// index.js

ReactDOM.hydrate(<App name="Saeloun"/>, document.getElementById('root'));

//server.js

import React from "react";
import ReactDOMServer from "react-dom/server";

app.use("/", (req, res, next) => {
  fs.readFile(path.resolve("./build/index.html"), "utf-8", (err, data) => {
    if (err) {
      console.log(err);
      return res.status(500).send("Some error happened");
    }
    return res.send(ReactDOMServer.renderToString(<App name="Saeloun" />)
    )
  });
});

//App.js

import React from "react";

function App(props) {
  return (
    <div>
      Hello {props.name}!
    </div>
  )
}

export default App;

The output of the application before loading scripts on the client-side:

<html>
  <head></head>
  <body>
    <div id="root">
      <h1>Hello Saeloun!</h1>
    </div>
  </body>
</html>

Now, consider an example mentioned in the SSR discussion.

We want to show a fully interactive app when the app is ready.

The green color conveys that these parts of the page are interactive i.e. all the event handlers are attached ( JS is fully loaded ).

In CSR, the only thing the user will see while JavaScript is loading is a blank page, decreasing the user experience.

In SSR, we render React components on the server into HTML and send them to the user. HTML is not very interactive. However, it lets the user see something while the JavaScript is still loading:

Here, the grey color illustrates that these parts of the screen are not fully interactive. The JavaScript code has not loaded yet, so clicking buttons doesn’t do anything.

We tell React to attach event handlers to the HTML to make the app interactive. This process of rendering our components and attaching event handlers is known as “hydration”. It is like watering the ‘dry’ HTML with the ‘water’ of interactivity and event handlers. After hydration, our application becomes interactive, responding to clicks, and so on.

React expects that the rendered content is identical between the server and the client. It can patch up differences in text content. Mismatches must be treated as bugs and should be fixed. In development mode, React warns about mismatches during hydration.

If a single element’s attribute or text content is unavoidably different between the server and the client (for example, a timestamp), we can silence the warning by adding suppressHydrationWarning={true} to the element. But, it should not be overused!

If we intentionally need to render something different on the server and the client, we can do a two-pass rendering. Components that render something different on the client can read a state variable like this.state.isClient, which can be set to true in componentDidMount(). It will render the same content as in the server, avoiding mismatches. But an additional pass will happen synchronously right after hydration. This approach makes our application slower, so should be used with caution.

Benefits of using Hydration

  • Improves SEO
  • Decreases the initial load time

Need help on your Ruby on Rails or React project?

Join Our Newsletter