How can partial hydration, progressive hydration be accomplished in React Part 2
May 15, 2021
Before we begin, there is no agreed upon method as of this writing of how to progressively hydrate an application. The following content is what I have found while looking into the topic of progressive hydration, before working with my colleagues to come up with our own way of progressive hydration for our application.
When hydrating, we are trying to attach interactivity back to the DOM. There are 2 main questions, when and how.
How do you progressively hydrate?
- The application splits the codebase into 2 categories, the first category is code that runs on server side, and the second category is code that runs on client side.
The server-side implementation of the deferHydration
function performs the import
synchronously and returns a higher-order component that simply wraps the desired React component. The client-side implementation utilizes the fact that React’s dangerouslySetInnerHTML
prop is ignored during the hydration step to “match” the hydrated tree. This “match” is important because it prevent React from wiping its server-rendered contents. One of the most common SSR Rehydration pitfalls is where a server-rendered DOM tree gets destroyed and then immediately rebuilt.
- At the client side, this component registers a listener upon instantiation and never updates during additional renders, so the React tree continues to ignore it. When the listener is triggered, the JavaScript for the deferred component is downloaded and a new React tree is instantiated and hydrated at that DOM node.
When can we rehydrate?
We can rehydrate on hydration triggers. Here are some examples of possible hydration “triggers”.
- view (hydration is triggered when the component scrolls into the viewport)
- never (plain un-hydrated HTML has full functionality and no code is loaded or executed)
- click/hover/focus (hydrate when the user interacts with the component, then repeat the event so React can also respond to it)
- interaction (hydrate the component when the user interacts with the page in any way)
- an arbitrary Promise (custom hydration trigger behaviour)
How do you pass data that event listeners need?
Some of these interactivity requires data from the app at runtime. For instance, an interactive action of adding a product to cart would likely require the product ID. Depending on your hydration strategy, you may
Normally, these details can easily be fetched from the redux store (if you are using react with redux) or the component’s state (if you are using a reducer hook instead). With progressive hydration, you’ll need to store that data somewhere so your client side code can access it. After all, doing different work on the server and client would result in the server markup being thrown out by React’s initial reconciliation. To get around this, you may do the following:
- rendering a wrapping
div
around the content on the server and then adding an ID to that element, and setting thedangerouslySetInnerHTML
prop on the client to the resulting server markup in order to avoid mismatches. Once you have done whatever work on the client to load the necessary components, this hardcoded markup is removed, allowing the React tree to take over. - pass data via windows object. You can assign the data to window object and then read it in React application.
- pass data via attribute on parent node and then on client-side, find that specific data attribute tag to get your values.
<div
id="react"
data-react='{"user":{"name":"BTM","email":"example@example.com"}}'
></div>
<script src="react-application.js"></script>
const node = document.getElementById("root")
const userData = node.dataset.react ? JSON.parse(node.dataset.react) : {}
ReactDOM.hydrate(<App user={userData.user} />, node)
With these concepts in mind, here is some code examples to demonstrate how it is done. In this code sandbox, we have our normal React application without progressive hydration.
And here is how a progressive hydrated React application could look like.
Notice the following:
- The following code is the code ran on client-side. We assume that the page’s HTML is generated on the server and the user’s browser (also known as the client-side) receives the generated HTML. The initial HTML is located at
public/index.html
. On reaching client-side, the hydration kick-starts. - You can see interactivity injected in
showcase.js
withinuseEffect
. We go primitive and query for the DOM elements we need to attach event listeners to, all with the help of custom data attributes. - On tab change, we mount the next React component and give control back to React framework again.
- Event listeners are all attached in
Showcase
page component, which increases the coupling between components.
Difference in thought process when using progressive hydration
If you choose to embark on progressive hydration, using our current popular front end frameworks may have limited powers. Your application will have to be built with data flow in mind since data will now persist in the DOM that the server-side rendered HTML sent over. You component architecture may also change. In the above progressive hydrated React application CodeSandBox example, we can see that we our Tab
and Card
component interactivity had to be added in Showcase
component instead of their own. We can try to decouple the Tab
component from Showcase
component by restructuring the component hierarchy as follows:
function Showcase() {
return (
<>
<Tab />
<Slider />
</>
)
}
function Tab() {
const ref = createRef()
const isSubsequentRender = useTabInteraction(ref)
return (
<div
ref={ref}
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: "" }}
/>
)
}
function Slider() {
const ref = createRef()
const modal = useAddToCartInteraction(ref)
useSliderInteraction(ref)
useReactRouterLink(ref)
return (
<>
<div
ref={ref}
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: "" }}
/>
{modal}
</>
)
}
I can’t think of any way to decouple
Card
component fromShowcase
component at the moment.
Closing note on issues hydrating a subtree
There are still many issues with this progressive hydrated React application. If you have played around with it, you will notice that on navigation back to showcase
page, the page returns a black. This is because the initial rendered HTML gone after a page re-render and the current client side component does not re-render showcase
component because it only tackles hydration.
There are some more issues of building a progressive hydration model upon our current React framework (v17) documented roughly here. Fortunately, the React team is also looking into something similar with Server Components.