TLDR: "Hydrating" React apps can result in errors. I published an NPM package to fix them: react-hydration-provider
In recent years it has become very common to use Server-Side Rendering (SSR) and Static Site Generation (SSG) for frontend React apps. This can be great for user experience and SEO, but sometimes it also leads to some strange error messages, such as:
- Warning: Text content did not match. Server: "Pre-rendered server content" Client: "Client app content" at div.
- Warning: An error occurred during hydration. The server HTML was replaced with client content in div.
- Text content does not match server-rendered HTML.
- Hydration failed because the initial UI does not match what was rendered on the server.
- There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
The Problem
The reason behind these errors is a mismatch between the HTML provided by the server and what is generated by the frontend React app. For hydration to work correctly, the HTML must be exactly identical.
It is extremely easy to make this mistake without realizing it. Anything dynamic in your app may be a potential culprit. Here is a practical example of when this might occur:
function PracticalHydrationError({ theDate }) {
const formatted_date = new Date(theDate).toLocaleDateString();
return <div>{formatted_date}</div>;
}
In this component, a date is formatted to the user's region-specific date format. The problem here is that the server may not be using that same format, and the date on the frontend will not match what was provided by the backend.
Even worse, it may only be a mismatch for some users. This means that if the date format for your local region happens to match that of the server, you may not even see this error in production, but it will still be affecting your users who are in different regions.
Solution #1: Remove content from initial render
Unfortunately there is no magic fix for this issue, but there are options for preventing these errors and improving the UX of your web app.
The first solution is to simply prevent certain parts of your app from rendering on the server side. This is simple to accomplish with useEffect():
export default function NoFirstRender({ theDate }) {
const [hydrated, setHydrated] = React.useState(false);
React.useEffect(() => {
// This forces a rerender, so the date is rendered
// the second time but not the first
setHydrated(true);
}, []);
if (!hydrated) {
// Returns null on first render, so the client and server match
return null;
}
const formatted_date = new Date(theDate).toLocaleDateString();
return <div>{formatted_date}</div>;
}
If you're familiar with useEffect(), that passing an empty dependency array results in function only being called when the component is first mounted. Calling setHydrate(true), causes the component to render twice. This code prevents the date from rendering on the initial load, which means the date text will be excluded from the HTML that is generated on the server. When it is loaded on the frontend, the hydration process will also use this same HTML with the date excluded, since on the first render hydrated is false. This prevents the error from occurring. Then, immediately following hydration, the formatted locale date will be rendered as usual.
From a user's perspective, this means the page will initially be displayed without the date and suddenly appear once the app has loaded.
Solution #2: Render different content for client and server
In some cases, the first solution may not be ideal. Using this same date example, it may be important that your site always renders a date for SEO and UX purposes or maybe you just don't want to leave that space blank before the client-side app has loaded.
This is essentially the same as the first solution, except you return JSX or a component instead of null:
export default function DifferentServerAndClient({ theDate }) {
const [hydrated, setHydrated] = React.useState(false);
React.useEffect(() => {
setHydrated(true);
}, []);
// Instead of returning null, we convert the date to
// UTC format on the server, which should be the same
// for all users.
// Then we still use the locale format once the app is hydrated.
const date = new Date(theDate);
const formatted_date = hydrated ? date.toLocaleDateString() : date.toUTCString();
return <div>{formatted_date}</div>;
}
Solution #3: Preventing unnecessary renders
This solution using useEffect() works, but it does have a problem.
The purpose of useEffect() has nothing to do with hydration. Passing the empty dependency array ([]) makes the function run when the component is first mounted. This is a problem, because components may be mounted/unmounted multiple times as a user navigates throughout the app.
To solve this, we can use a React Context and Provider to ensure the hydration check only runs once when the app is first initialized:
const HydrationContext = React.createContext(false);
function HydrationProvider({ children }) {
const [hydrated, setHydrated] = React.useState(false);
React.useEffect(() => {
setHydrated(true);
}, []);
return <HydrationContext.Provider value={hydrated}>{children}</HydrationContext.Provider>;
}
function MyDateComponent({ theDate }) {
// Retrieve the hydration state from the context
const hydrated = React.useContext(HydrationContext);
const date = new Date(theDate);
const formatted_date = hydrated ? date.toLocaleDateString() : date.toUTCString();
return <div>{formatted_date}</div>;
}
export default function OnlyRerenderAfterHydration() {
return (
<HydrationProvider>
<MyDateComponent />
</HydrationProvider>
);
}
By moving the useEffect() check into a Provider component, we avoid needing to call it every time our date component is mounted.
If we place <HydrationProvider> at the highest level of our app, the hydration check will only run once when the app is actually hydrated, rather than every time a component mounts.
So, for example, if you were using <MyDateComponent> numerous times throughout your app, using this Provider solution would significantly reduce the number of renders that need to be performed.
react-hydration-provider NPM package
To make all of this easier, I published an NPM package that does everything for you.
You can use it by running:
yarn add react-hydration-provider
or
npm install react-hydration-provider
Then then all you need in your app is something like this:
import { HydrationProvider, Server, Client } from "react-hydration-provider";
function App() {
return (
<HydrationProvider>
<main>
<Server>
<p>
This will be rendered during html generation (SSR, SSG, etc) and the initial app hydration. It should always
have a reliable value that will render the same in both a server and client environment.
</p>
</Server>
<Client>
<p>This will be rendered after initial app hydration.</p>
<p>It can safely contain dynamic content, like this: {Math.random()}</p>
</Client>
<p>This will always be rendered.</p>
</main>
</HydrationProvider>
);
}
It also includes a useHydrated() hook that can be used to implement your own logic:
import { HydrationProvider, useHydrated } from "react-hydration-provider";
function MyComponent() {
const hydrated = useHydrated();
return hydrated ? <p>Client render</p> : <p>Server render</p>;
}
function App() {
return (
<HydrationProvider>
<main>
<MyComponent />
</main>
</HydrationProvider>
);
}
For more examples and documentation, you can view the package on NPM or Github.