A blog by Aaron Godin

Server-Side Caching of React Components

A difficult part of using React in a broader architecture is that React needs to be aware of the entire DOM below its root element. This allows it to maintain an in-memory tree of the DOM, which is the special sauce in React. When you’re dealing with cached fragments of HTML that are part of a larger React application, here are a few design principles I’ve been following.

The tools I’m going to be using to illustrate the example are React (of course!), Node & Express. I’m also going to assume that the caching bit of our architecture is already taken care of, most likely through a CDN such as Akamai.

Let’s look at an example React application that renders many products with prices.

import React from 'react';

const Price = ({ price }) => {
  return (
    <div className='price'>{price.amount}</div>
  );
};

const Product = ({ product }) => {
  return (
    <div className='product'>
      <h3>{product.title}</h3>
      <p>{product.description}</p>
      <Price price={product.price} />
    </div>
  );
};

class Main extends React.Component {
  render () {
    // We receive an array of product through props, perhaps from a redux store
    const products = this.props.products.map((product, productIndex) => {
      return <Product key={`product-${productIndex}`} product={product} />
    });

    return (
      <main class='list-page'>
        {products}
      </main>
    );
  }
}

Our Main component renders an array of products, and each product renders a price for that product. The section we’d like to cache for this application is each Product element—they don’t change extremely often, and when they do, it’s not unreasonable to wait 10 minutes or so for them to fall out of cache. How might we go about this?

Creating The Fragment

When we render our Main component server-side, we need to be able to pass the cached fragments of HTML for each product. While this will necessitate a change to our application above, let’s first address the issue of creating the fragment.

In this example, the application that renders the Main component is an Express app that is posted data and responds with HTML. For starters, we could simply break the Product and Price components into a separate module that is rendered through a separate endpoint in our web server. ReactDOM‘s renderToString() will help us.

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { Product } from './component/product';

// app is a reference to an Express application

app.post('/fragment/product', (req, res) => {
  // ... and we set up that application to use body-parser by default
  const productProps = req.body.product;

  const renderedFragment = ReactDOMServer.renderToString(
    React.createElement(Product, productProps)
  );
});

One note I’ll make about this example is that, in certain cases it’s more idiomatic and easier to understand when you simply call createElement() yourself, rather than bringing in JSX. I find I do this more on the server, especially when it’s for creating a single top-level element.

Using Our Fragment

The responsibility of the Main component is still the same as it was before, but the underlying implementation has shifted. Now, instead of rendering the components themselves, we’ll change it to render the list of HTML fragments we created.

import React from 'react';

class Main extends React.Component {
  render () {
    // Now assuming we have a fragments prop that is an array of strings
    const fragments = this.props.fragments.map((fragment, index) => {
      return <div
        key={`fragment-${index}`}
        dangerouslySetInnerHTML={{ __html: fragment }} />
    });

    return (
      <main class='list-page'>
        {fragments}
      </main>
    );
  }
}

Well, that looks a little strange. React has a property on all DOM elements called dangerouslySetInnerHTML. Appropriately named, it let’s you override the innerHTML of a DOM element to provide your own. The reason it’s dangerous is that React cannot do anything with the DOM structure underneath that <div>, which means you’re essentially losing that secret sauce I was talking about.

The primary issue we have now is that given a Product component with any client-side functionality (browser event handlers, for example), we’ve lost that through inlining the product’s cached HTML in our application.

Differences Between Server and Client

While writing JavaScript in a universal way, I try to avoid what we’re about to do. That said, in some cases it’s impossible to avoid some differences between your client- and server-rendered code.

To solve our client-side issue, we will turn Product into its own React application, nested within our main application. I’ve designed a React component that renders other React components as root elements, allowing you to deeply nest React applications when necessary.

import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom';

class CachedFragment extends React.Component {
  componentDidMount () {
    const {
      component,
      fragmentProps
    } = this.props;

    const { cachedFragmentMount } = this.refs;

    ReactDOM.render(
      React.createElement(component, fragmentProps),
      cachedFragmentMount
    );
  }

  shouldComponentUpdate () {
    return false;
  }

  render () {
    return (
      <div ref='cachedFragmentMount'
        dangerouslySetInnerHTML={{ __html: this.props.fragment }} />
    );
  }
}

CachedFragment.propTypes = {
  fragment: PropTypes.string,
  component: PropTypes.func,
  fragmentProps: PropTypes.object
};

The CachedFragment component takes advantage of two of React’s lifecycle methods, allowing us to have different behavior for the client and server. When server-rendered, React simply calls render() and returns the string of markup. When client-rendered, componentDidMount() is called once and only once. This allows us to hook into the ref of our <div> inside of render(). Using the ref as a mount point for ReactDOM.render(), we can hook up any client-side functionality for the component.

The last important piece is overriding shouldComponentUpdate(). When a React component receives new props, it looks at shouldComponentUpdate() first to allow the user to decide if React can skip this update. This is useful for optimizing cases where a component might receive new props, but you know the output of render() will not change. In this case, we simply return false as React does not need to re-render our mount point ever again.

Circling back to our Main component from before, let’s now render our products using the CachedFragment component.

import React from 'react';
import { Product } from './component/product';
import { CachedFragment } from 'shared-components/cached-fragment';

class Main extends React.Component {
  render () {
    const fragments = this.props.fragments.map((fragment, index) => {
      return (
        <CachedFragment
          key={`fragment-${index}`}
          component={Product}
          fragment={fragment.cachedContent}
          fragmentProps={fragment.props} />
      );
    });

    return (
      <main class='list-page'>
        {fragments}
      </main>
    );
  }
}

Wrapping Up

This example is just one way to solve caching, and it was specifically born out of working with CDNs. It certainly doesn’t fit every possible caching strategy. Even though we are abusing React’s architecture to a certain extent, abstracting away the difficult parts into something that is predictable will at least circumvent any confusion, and still allow you to implement the sort of HTML fragment caching that helps scale extremely high-traffic websites.