If you're upgrading to React 18 and have run into the following error.
Text content does not match server-rendered HTML
This post should help explain what causes the error, and a couple of solutions.
I recently ran into this issue myself when building this demo for the following blog post: How to use Serverless Functions with SSR.
The reason was because I was using JavaScript's Date()
constructor in a few components to render a date.
The examples discussed in this post will mainly focus on dates, but this error could occur in many different client/server scenarios.
In my case the error occurred when using dates because of a mismatch with the date, or more specifically the time, (in seconds).
When Gatsby/React first renders a page on the server and the Date()
constructor is used, the date output includes
seconds. Then, shortly after the initial page load, hydration occurs. During this period the elapsed time has changed,
therefore the seconds are different. This leads React to believe the "text" is different between server and client
renders, and to be fair to React, it is.
The two solutions I'll be discussing will help rid you of the error by waiting for hydration to occur before attempting to initialize the date constructor, but all will present you with a new problem.
If you wait for hydration to occur before calling new Date()
you'll first render a page where the HTML that displays
the date will be blank. This can affect Lighthouse CLS scores. In most
cases this can be overcome by adding CSS to ensure the width or height of the HTML element doesn't change. However, it
will still leave you, initially with an empty element that "pings" into view after hydration. And lastly, should a user
have JavaScript disabled in the browser the elements will again be empty.
As mentioned all of the following "solutions" will only prevent React from surfacing the error, but extra CSS solutions will be required to overcome the new problems these solutions create.
This approach uses React's built in Suspense
method. Suspense lets components “wait” for something before rendering.
// src/pages/index.js
import React, { Suspense } from 'react';
const Page = () => {
return (
<main>
<h1>Page</h1>
<time>
<Suspense fallback={null}>{new Date().toLocaleDateString()}</Suspense>
</time>
</main>
);
};
export default Page;
This approach comes with a warning. The result of a hook will cause a page to re-render this could lead to performance issues because React will render the page once on initial hydration, and then again as a result of the hook. This will happen for every "page" where the hook is implemented.
// src/pages/index.js
import React from 'react';
import { useHydrationSafeDate } from '../hooks/use-hydration-safe-date';
const Page = () => {
const date = useHydrationSafeDate(new Date());
return (
<main>
<h1>Page</h1>
<time>{date}</time>
</main>
);
};
export default Page;
// src/hooks/use-hydration-safe-date.js
import { useState, useEffect } from 'react';
export const useHydrationSafeDate = (date) => {
const [safeDate, setSafeDate] = useState();
useEffect(() => {
setSafeDate(new Date(date).toLocaleDateString());
}, [date]);
return safeDate;
};
This approach is a little more involved as it requires the use of React Context API. However, wrapping your site in a Context Provider will mean the re-render after hydration will only happen once, unlike the Hydration Safe Hook approach mentioned above.
The following demonstrates how to wrap a site using Gatsby specific methods.
// src/context/app-context.js
import React, { createContext, useEffect, useState } from 'react';
export const AppContext = createContext();
export const AppProvider = ({ children }) => {
const [isHydrated, setIsHydrated] = useState(false);
useEffect(() => {
setIsHydrated(true);
}, []);
return <AppContext.Provider value={{ hydrated: isHydrated }}>{children}</AppContext.Provider>;
};
// src/pages/index.js
import React from 'react';
import { AppContext } from '../context/app-context';
const Page = () => {
return (
<main>
<h1>Page</h1>
<time>
<AppContext.Consumer>
{({ hydrated }) => {
return hydrated ? new Date().toLocaleDateString() : '';
}}
</AppContext.Consumer>
</time>
</main>
);
};
export default Page;
// src/components/root-element.js
import React from 'react';
import { AppProvider } from '../context/app-context';
const RootElement = ({ children }) => {
return <AppProvider>{children}</AppProvider>;
};
export default RootElement;
// ./gatsby-browser.js
import React from 'react';
import RootElement from './src/components/root-element';
export const wrapRootElement = ({ element }) => {
return <RootElement>{element}</RootElement>;
};
// ./gatsby-ssr.js
import React from 'react';
import RootElement from './src/components/root-element';
export const wrapRootElement = ({ element }) => {
return <RootElement>{element}</RootElement>;
};