React hydration error 425 "Text content does not match server-rendered HTML"
If youâre upgrading to React 18 and have run into the following error, this post should help explain what causes the error, and a couple of solutions.
Text content does not match server-rendered HTML
If youâd like to code along at home Iâve prepared a small reproduction repo with a branch for each solution.
- đ Live Preview (with errors)
- âď¸ main
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 solutions discussed in this post will mainly focus on dates, but this error could occur in many different client/server scenarios.
The Problem
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 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 this will likely present you with a new problem.
The New Problem
If you wait for hydration to occur before calling new Date()
youâll first display the page with an empty HTML element.
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.
Solutions
As mentioned, most 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. There are also a couple of things to consider before implementing any of the solutions. Iâve done my best to explain them but feel free to comment if Iâm mistaken or anything is unclear.
Iâve also included a bonus / Gatsby specific approach to handling dates. This however is dependant on where youâre souring data and if youâre able to query the date using GraphQL.
Suspense
This approach uses Reactâs built in Suspense
method. Suspense lets components âwaitâ for something before rendering.
Page
// 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;
Hydration Safe Hook
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.
Page
// 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;
Hook
// 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;
};
Hydration Provider
This approach is a little more involved as it requires the use of Reactâs 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 with a âProviderâ using Gatsby specific methods.
App Context
// 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>;
};
Page
// 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;
RootElement
// 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
// ./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
// ./gatsby-ssr.js
import React from 'react';
import RootElement from './src/components/root-element';
export const wrapRootElement = ({ element }) => {
return <RootElement>{element}</RootElement>;
};
Format String
If your dates are sourced from local .md/.mdx
files or a CMS then using GraphQLâs built-in method circumvents this
problem entirely as dates are rendered at build time and can be statically returned as a string.
I have however seen folks query a date using GraphQL and then use a Date()
constructor with toLocaleDateString()
to
format the date.
Donât do this!
import React from 'react';
import { graphql } from 'gatsby';
const Page = ({
data: {
markdownRemark: {
frontmatter: { date }
}
}
}) => {
return <time>{new Date(date).toLocaleDateString()}</time>
);
};
export const query = graphql`
query ($id: String!) {
markdownRemark(id: { eq: $id }) {
frontmatter {
date
}
}
}
`;
export default Page;
Instead, you can format the date using GraphQL.
Date locales however arenât possible as you need to decide which date format youâd like to use ahead-of-time. There are a number of ways you can choose to format dates using GraphQL. Iâve included x3 three options in this example.
md
// content/index.md
---
date: 2022-10-20
---
# Page
Page
// src/pages/{MarkdownRemark.parent__(File)__name}.js
import React from 'react';
import { graphql } from 'gatsby';
const Page = ({
data: {
markdownRemark: {
frontmatter: { title, date, dateShort, dateLong },
},
},
}) => {
return (
<main>
<h1>{title}</h1>
<time>{date}</time>
<time>{dateShort}</time>
<time>{dateLong}</time>
</main>
);
};
export const query = graphql`
query ($id: String!) {
markdownRemark(id: { eq: $id }) {
frontmatter {
title
date(formatString: "DD/MM/YYYY")
dateShort: date(formatString: "MMM DD, YYYY")
dateLong: date(formatString: "MMMM DD, YYYY")
}
}
}
`;
export default Page;
The date output for the x3 formats are as follows.
date
= 20/10/2022dateShort
= Oct 20, 2022dateLong
= October 20, 2022
As youâll see at the top of this page, my preference is to use the dateLong
format. Iâve found this to be the one that
makes most sense for me. The slight drawback of course is the month name is always in English.
Feedback Welcome!
If you know of any other solutions, or if thereâs any issues with using the above methods that Iâve overlooked, please feel free to drop a comment below!