πŸ’§ 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 []

πŸ– Combining React Client and Render Server for SSR

Photo by Mike Petrucci on Unsplash

This is part 6 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. [You are here] πŸ– Combining React Client and Render Server for SSR
  7. ⚑️ Static Router, Static Assets, Serving A Server-side Rendered Site
  8. πŸ’§ Hydration and Server-side Rendering
  9. 🦟 Debugging and fixing hydration issues
  10. πŸ›‘ React Hydration Error Indicator
  11. πŸ§‘πŸΎβ€πŸŽ¨ Render Gateway: A Multi-use Render Server

Here we are at last? Finally, a server-side rendered application? We shall see1. After making a simple client-side React application and talking about, building, and extending a server-side rendering (SSR) architecture, we can do the work of combining the two.

Last time we took our simple express server and got it to return an HTML page that included some actual rendered React. It was not particularly flashy, but it did prove out the concept. So, what now? Well, we need to somehow have our server-side code render our client-side code.

πŸ₯£ Combining the server and client

Currently, these are two separate projects. One, our client-side app, is using JSX; the other, our server-side app, does not know what JSX is. To combine them, we need to either teach the server about JSX or incorporate a built version of the client app.

My personal preference is to keep the server as unaware of the client-side specifics as possible. This has the advantage that in the long term, the server might even need to know that it is rendering React at all. However, before we resort to that, let's try to compromise a little for the sake of keeping things simple.

Incorporating our client app into our server app is not quite so simple as just copying the code over. Our server needs to understand it. Thankfully, it only needs to understand how to turn JSX into regular JS, so we can add our own Babel configuration for that. Since the client app is in its own Git repository, we can import a version of it via commit SHA. However, we do need the production build of the template, which creates a problem since we cannot build the client application in the server repository; it has developer dependencies that won't be fulfilled in the server project.

The traditional way to resolve that would be to publish the client app as a package in a package repository. That way the built template would be a part of the package for us to include. Of course, we are not going to do that; it's far more work than I want to do right now. Instead, what we will do is have both packages co-exist in the same repository as siblings. The server can then easily invoke commands on the client application if it so requires in order to ensure access to the client production assets. This allows both server and client to exist on their own while still allowing for the integration we seek2.

To get the server able to compile the JSX files, I ran the following commands, added the given Babel config file, and modified our package.json scripts to add babel-watch to the start command so that we can edit code and auto-transpile and restart3. With these changes, I then updated the index.js server file to replace our previous hack of using React.createElement with some actual JSX.

yarn add --dev @babel/cli @babel/core @babel/proposal-plugin-class-properties @babel/preset-env @babel/preset-react
{
  "name": "hello-react-world-ssr",
  "version": "0.0.1",
  "description": "A server-side rendering server",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "express": "^4.17.1",
    "react": "^16.12.0",
    "react-dom": "^16.12.0"
  },
  "scripts": {
    "start": "NODE_ENV=development babel-watch index.js"
  },
  "devDependencies": {
    "@babel/cli": "^7.8.4",
    "@babel/core": "^7.8.4",
    "@babel/plugin-proposal-class-properties": "^7.8.3",
    "@babel/preset-env": "^7.8.4",
    "@babel/preset-react": "^7.8.3",
    "babel-watch": "^7.0.0"
  }
}
const express = require("express");
const React = require("react");
const {renderToString} = require("react-dom/server");
const {getPageTemplate} = require("./get-page-template.js");

const port = 3000;
const app = express();

const pageTemplate = getPageTemplate();

const renderPage = (reactComponent) => {
    const renderedComponent = renderToString(reactComponent);
    return pageTemplate.replace('<div id="root"></div>', `<div id="root">${renderedComponent}</div>`);
};

app.get("/*", (req, res) => res.send(
    renderPage(<div>Hello World!</div>),
));

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

Running yarn start demonstrates that this updated code is working, so our final step is to incorporate our actual App component. This is as simple as importing the App component and then replacing the <div>Hello World!</div> with <App />. Great! yarn start again.

Sadly, this immediately results in an error when the App component tries to import the SVG logo. To work around this, we can extend Node's require to support the SVG extension (while we're at it, we also have to do this for the CSS imports).

const fs = require("fs");

const requireText = function (module, filename) {
    module.exports = fs.readFileSync(filename, 'utf8');
};

require.extensions[".svg"] = requireText
require.extensions[".css"] = requireText;

Now when we yarn start, things appear to be all right. However, if we navigate to localhost:3000, instead of any part of our app, we get an error:

Error: Invariant failed: Browser history needs a DOM
    at invariant (/hello-react-world/client/node_modules/tiny-invariant/dist/tiny-invariant.cjs.js:13:11)
    at Object.createBrowserHistory (/hello-react-world/client/node_modules/history/cjs/history.js:273:16)
    at new BrowserRouter (/hello-react-world/client/node_modules/react-router-dom/modules/BrowserRouter.js:11:13)
    at processChild (/hello-react-world/server/node_modules/react-dom/cjs/react-dom-server.node.development.js:3159:14)
    at resolve (/hello-react-world/server/node_modules/react-dom/cjs/react-dom-server.node.development.js:3124:5)
    at ReactDOMServerRenderer.render (/hello-react-world/server/node_modules/react-dom/cjs/react-dom-server.node.development.js:3598:22)
    at ReactDOMServerRenderer.read (/hello-react-world/server/node_modules/react-dom/cjs/react-dom-server.node.development.js:3536:29)
    at renderToString (/hello-react-world/server/node_modules/react-dom/cjs/react-dom-server.node.development.js:4245:27)
    at renderPage (/hello-react-world/server/index.js:14:31)
    at app.get (/hello-react-world/server/index.js:19:5)

Oh noes! Indeed, we forgot that our app currently renders inside a BrowserRouter component and we don't have a browser with history on our render server. Of course, we could fake one, but there is no need as React Router already has us covered with StaticRouter. However, in order to use that, we're going to need to change our client-side component a little to accommodate our server expectations.

Next time, we will introduce the StaticRouter, and see that it leads to more things that we must address to get our application working the way we want. Until then, please question, comment, and discuss. The aim of this whole series is to learn more about how we can leverage SSR. That means we will continue to get messy with doing things the hard way and build an appreciation for the easy way we hopefully uncover. πŸ•΅πŸ»β€β™‚οΈ

  1. Hint: the answer is "sort of". []
  2. Knowledgeable folks among you will already see problems with this approach; we shall get to those []
  3. Eventually, we may want to build our server so that we do not need babel-watch for production usage; for now, it is not important. []

C#6: Auto-property Initializers and Expression-Bodied Properties

Last week, I discussed the new null-conditional operators added in C#6. This week, I would like to discuss two features that are awesome but could lead to some confusion: auto-property initializers and expression-bodied properties1.

Auto-initialized Properties

Before C#6, if we wanted to properly define an immutable property that had some expensive initialization, we had to do the following2:

public class MyClass
{
    public MyClass()
    {
        _immutableBackingField = System.Environment.CurrentDirectory;
    }

    public string ImmutableProperty
    {
        get
        {
            return _immutableBackingField;
        }
    }

    private readonly string _immutableBackingField;
}

Some people often use the shortcut of an auto-implemented property using the following syntax:

public class MyClass
{
    public MyClass()
    {
        ImmutableProperty = System.Environment.CurrentDirectory;
    }

    public string ImmutableProperty
    {
        get;
    }
}

However, defining properties like this means they are still mutable within the class (and its derivations). Using a backing field with the `readonly`Β keyword not only ensures that the property cannot be changed anywhere outsideΒ of the class construction, it also expresses exactly what you intended. Being as clear as possible is helpful for anyoneΒ who has to maintain the code in the future, including your future self.

From what I have read and heard, the main driver for using auto-implemented properties was writing less code. It somewhat saddens me whenΒ clarity of intent is replaced by speed of coding as we often pay for it later. Thankfully, both can now be achieved using initializers. Using this new feature,Β we can condense all that code down to just this:

class MyClass
{
    public int ImmutableProperty { get; } = System.Environment.CurrentDirectory;
}

It is a thing of beauty3.Β Behind the scenes, the compiler produces equivalentΒ code to the first example with the `readonly` backing field.

Of course, this doesn't help much if you need to base your initialization on a value that is passed in via the constructor. Though a proposed feature for C#6, primary constructors, would have helped with this, it was pulled from the final release. Therefore, if you want to use construction parameters, you will still need a backing field of some kind. However, there is another feature that can helpΒ withΒ this. That feature is expression-bodied properties4.

Expression-bodied Properties

An expression-bodied property looks like this:

class MyClass
{
    public int ImmutableProperty => 42;
}

This is equivalent to:

public class MyClass
{
    public int ImmutableProperty
    {
        get
        {
            return 42;
        }
    }
}

Using this lambda-esque syntax, we can provide more succinct implementations of our read-only properties.Β Consider this code:

public class MyClass
{
    public MyClass(string value)
    {
        _immutableBackingField = value;
    }

    public string ImmutableProperty
    {
        get
        {
            return _immutableBackingField;
        }
    }

    private readonly string _immutableBackingField;
}

Using expression-body syntax, we can write it as:

public class MyClass
{
    public MyClass(string value)
    {
        _immutableBackingField = value;
    }

    public string ImmutableProperty => _immutableBackingField;

    private readonly string _immutableBackingField;
}

But for the additional backing field declaration, this is almost as succinct as using an auto-implemented property. Hopefully, this new syntax will encourage people to make their intent clear rather than using the auto-implemented property shortcut when implementing immutable types.

Caveat Emptor

These new syntactical enhancements make property declaration not only easier to write, but in many common cases, easier to read. However, the similarities in these approaches can lead to some confusing, hard-to-spot bugs. Take this code as an example:

using System;

public class MyClass
{
    public string CurrentDirectory1 { get; } = Environment.CurrentDirectory;
    public string CurrentDirectory2 => Environment.CurrentDirectory;
}

Here we have two properties: `CurrentDirectory1` and `CurrentDirectory2`. Both seem to return the same thing, the current directory. However, a closer look reveals a subtle difference.

Imagine if the current directory is `C:\Stuff` at class instantiation but gets changed to `C:\Windows` some time afterward; `CurrentDirectory1` will return `C:\Stuff`, but `CurrentDirectory2` will return `C:\Windows`.Β The reason for this difference is the syntax used. The first property uses auto-initialization; it captures the value of `Environment.CurrentDirectory` on construction and always returns that captured value, even if `Environment.CurrentDirectory` changes. The second property uses an expression-body; it will always return the current value of `Environment.CurrentDirectory`, not the value of `Environment.CurrentDirectory` onΒ construction of the `MyClass` instance.

I am sure you can imagine more serious scenarios where such a mix-up could be a problem. Do you think this difference in behavior will be obvious enough during code review or when a bug is reported? I certainly don't and I'm writing this as a way of reinforcing it in my own mind. Perhaps you have already dealt with a bug relating to this; if so, share your tale of woe in the comments.

In Conclusion..

I am by no means intending to discourage the use of these two additions to the C# language; they are brilliant and you should definitely add them to your coding arsenal, but like many things in software development, there is a dark side. Understanding the pros and cons of any such feature is important as it enables us to spot errors, fix bugs, and write good tests. This new confusion in the C# world is just another encouragement to code clearly, test sensibly, and be aware of the power in the tools and languages we use.

  1. No one else seems to by hyphenating "expression-bodied" but it doesn't make sense to me otherwise; what is a "bodied property"? []
  2. Yes, I know that `System.Enviroment.CurrentDirectory` isn't really expensive; this is for illustrative purposes []
  3. especially if you are keen on making sure your code expresses exactly what you mean []
  4. expression-bodied methods are also possible, but I'm not touching on that in this post []