‘react-progressive-enhancement’: A handy collection of HOCs for universally rendered apps
TL;DR: In universally rendered React apps, it is common to branch data-fetching and component-rendering depending on the environment (server or client), and defer rendering (a.k.a. “progressively enhance”) some components. However, we must ensure the first client render matches the server render. At Unsplash we have built handy higher-order components (HOCs) that encapsulate this pattern at https://github.com/unsplash/react-progressive-enhancement
Over the last year, the web team at Unsplash has gone through a long journey to dismantle a broken implementation of server-side rendering (SSR), and slowly replace it with a working one.
Our setup is the common “universal-rendering” one: we want to render the page on the server (for SEO & performance benefits), while also bundling the React app to the client to provide a smooth UX.
As we keep using the term “progressive enhancement”, I think it’s worth briefly explaining what we mean by that. Essentially, progressive enhancement is an approach that involves rendering essential content first, and leaving secondary content for later. Content is considered to be secondary either because it is not required for a good User Experience, or because there are technical limitations to rendering it at the start.
While working on universal rendering, there were a few regularly recurring problems we had to solve:
- data-fetching: when the client is performing an initial render right after a server render, it can avoid making data-fetching requests because the server has already made them. This saves a lot of bandwidth and resources. But when the user navigates to the page using client-side navigation, the data-fetching requests are required. We want to skip the client-side data requests when hydrating from a server-side render. In order to do that, we need to be able to differentiate between these two rendering modes.
- progressive enhancements 1/2 (deferring secondary content): some content isn’t considered “core content”, and so we can defer its rendering for the sake of a faster first-page load. Our page becomes more resilient as a result of fewer API calls to make. A good example of that on Unsplash is related photos:
- progressive enhancements 2/2 (client-specific data): Sometimes, components require data that isn’t available on the server, like viewport width or the
It’s worth looking at React’s docs concerning server renders:
React expects that the rendered content is identical between the server and the client. It can patch up differences in text content, but you should treat mismatches as bugs and fix them.
If React attempts to resolve the differences in rendered content, this can lead to degraded performance. Therefore, we must delay rendering the components described in the last two bullet points until after the first client render.
The React docs actually also explain how to work around this issue:
If you intentionally need to render something different on the server and the client, you can do a two-pass rendering. Components that render something different on the client can read a state variable like
this.state.isClient, which you can set to
componentDidMount(). This way the initial render pass will render the same content as the server, avoiding mismatches, but an additional pass will happen synchronously right after hydration. Note that this approach will make your components slower because they have to render twice, so use it with caution.
To avoid doing this individually for each component that needs
isClient, we need a way to share state across multiple components across the render tree. Let’s use React’s Context API for this, which is built for this expressed purpose.
First, we create the context and set the default value to
Second, we create an HOC that will wrap our root component with the provider. Keep in mind that we want
isEnhanced to be
true only after the client’s first render. Given that lifecycle hooks do not run on the server-side, the right place to update this boolean is in
componentDidMount (as explained in the React docs linked above):
And finally, we need two HOCs: one that will progressively render the provided component, and another one that simply adds
isEnhanced to the component’s props:
To use them in our codebase, we first wrap our root app component with
And then we’re free to use
progressivelyEnhance as follows:
The code shown here is essentially
react-progressive-enhancement, so if you like what you see, go ahead and use it! It comes with free TypesScript types 🙂
react-progressive-enhancement - A React Context that solves common SSR problemsgithub.com
If you like how we do things at Unsplash, consider joining us!