Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PaulieScanlon/aacea8cd9fc5ebf2b3479bfa0bf396c5 to your computer and use it in GitHub Desktop.
Save PaulieScanlon/aacea8cd9fc5ebf2b3479bfa0bf396c5 to your computer and use it in GitHub Desktop.
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.

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.

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 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.

The 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.

Solutions

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.

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 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.

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>;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment