ui icon indicating copy to clipboard operation
ui copied to clipboard

[bug]: Theme Provider creates hydration error in Next.js 15.0.1

Open TheOrcDev opened this issue 1 year ago • 34 comments

Describe the bug

Implementing dark mode, and putting ThemeProvider into the layout is making hydration error in newest version of Next.js (15.0.1).

Screenshot 2024-10-24 at 12 05 15

Affected component/components

ThemeProvider

How to reproduce

  1. Do npm install next-themes
  2. Create ThemeProvider component.
  3. Wrap children in layout file

Codesandbox/StackBlitz link

https://ui.shadcn.com/docs/dark-mode/next

Logs

Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used


-className="dark"
-style={{color-scheme:"dark"}}


### System Info

```bash
Next.js 15.0.1

MacOS, Google Chrome

Before submitting

  • [X] I've made research efforts and searched the documentation
  • [X] I've searched for existing issues

TheOrcDev avatar Oct 24 '24 10:10 TheOrcDev

Hey, as it is marked on the doc, you should add suppressHydrationWarning on your html tag :

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <>
      <html lang="en" suppressHydrationWarning>
        <head />
        <body>
          <ThemeProvider
            attribute="class"
            defaultTheme="system"
            enableSystem
            disableTransitionOnChange
          >
            {children}
          </ThemeProvider>
        </body>
      </html>
    </>
  )
}

Medaillek avatar Oct 24 '24 11:10 Medaillek

Hey, as it is marked on the doc, you should add suppressHydrationWarning on your html tag :

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <>
      <html lang="en" suppressHydrationWarning>
        <head />
        <body>
          <ThemeProvider
            attribute="class"
            defaultTheme="system"
            enableSystem
            disableTransitionOnChange
          >
            {children}
          </ThemeProvider>
        </body>
      </html>
    </>
  )
}

Thank you!

So this is not connected to Next.js 15? It's still that old problem we had with ThemeProvider?

TheOrcDev avatar Oct 24 '24 11:10 TheOrcDev

Hey, as it is marked on the doc, you should add suppressHydrationWarning on your html tag :

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <>
      <html lang="en" suppressHydrationWarning>
        <head />
        <body>
          <ThemeProvider
            attribute="class"
            defaultTheme="system"
            enableSystem
            disableTransitionOnChange
          >
            {children}
          </ThemeProvider>
        </body>
      </html>
    </>
  )
}

Thank you!

So this is not connected to Next.js 15? It's still that old problem we had with ThemeProvider?

Here is the real fix :

'use client'

import * as React from 'react'
const NextThemesProvider = dynamic(
	() => import('next-themes').then((e) => e.ThemeProvider),
	{
		ssr: false,
	}
)

import { type ThemeProviderProps } from 'next-themes/dist/types'
import dynamic from 'next/dynamic'

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
	return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

You have to dynamically import the theme provider from next-theme and you can now remove the suppressHydrationWarning from your <html> tag

Medaillek avatar Oct 24 '24 11:10 Medaillek

Hey, as it is marked on the doc, you should add suppressHydrationWarning on your html tag :

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <>
      <html lang="en" suppressHydrationWarning>
        <head />
        <body>
          <ThemeProvider
            attribute="class"
            defaultTheme="system"
            enableSystem
            disableTransitionOnChange
          >
            {children}
          </ThemeProvider>
        </body>
      </html>
    </>
  )
}

Thank you! So this is not connected to Next.js 15? It's still that old problem we had with ThemeProvider?

Here is the real fix :

'use client'

import * as React from 'react'
const NextThemesProvider = dynamic(
	() => import('next-themes').then((e) => e.ThemeProvider),
	{
		ssr: false,
	}
)

import { type ThemeProviderProps } from 'next-themes/dist/types'
import dynamic from 'next/dynamic'

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
	return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

You have to dynamically import the theme provider from next-theme and you can now remove the suppressHydrationWarning from your <html> tag

Amazing! That's exactly what I needed!

Thank you! You're a true web dev warrior!

TheOrcDev avatar Oct 24 '24 13:10 TheOrcDev

This solution is effective in preventing errors during SSR, but it isn’t an "elegant" approach. Dynamically loading the next-themes library can lead to undesired outcomes, such as a "snapshot of the wrong theme" at the start.

muhammetakalan avatar Oct 25 '24 07:10 muhammetakalan

This solution is effective in preventing errors during SSR, but it isn’t an "elegant" approach. Dynamically loading the next-themes library can lead to undesired outcomes, such as a "snapshot of the wrong theme" at the start.

That means that there is somewhere an issue, either with next-theme or with NextJs.

Now I think, I preffer having a "snapshot of the wrong theme" instead of suppressing all my hydration warning and troubleshouting during hours because nobody told me that i have put a <div> into an <p> in development mode.

But for the build, you're right, you could just suppress hydration warnings by using env like :

<html suppressHydrationWarning={process.env.NODE_ENV === 'production'}>
<>{children}</>
</html>

And then in your theme-provider.tsx :

'use client'

import * as React from 'react'
import dynamic from 'next/dynamic'

import { type ThemeProviderProps } from 'next-themes/dist/types'
import { ThemeProvider as StaticProvider } from 'next-themes'
const DynProvider = dynamic(
	() => import('next-themes').then((e) => e.ThemeProvider),
	{
		ssr: false,
	}
)

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
	const NextThemeProvider =
		process.env.NODE_ENV === 'production' ? StaticProvider : DynProvider
	return <NextThemeProvider {...props}>{children}</NextThemeProvider>
}

Medaillek avatar Oct 25 '24 09:10 Medaillek

'use client'

import * as React from 'react'
const NextThemesProvider = dynamic(
	() => import('next-themes').then((e) => e.ThemeProvider),
	{
		ssr: false,
	}
)

import { type ThemeProviderProps } from 'next-themes/dist/types'
import dynamic from 'next/dynamic'

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
	return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

Thanks! It seems to be the correct solution in general, although I must clarify that something different has worked better for me, understanding that it slows down rendering, in small projects I have not noticed any difference. On the other hand, using dynamic with ssr: false has generated a small flash at the start that I prefer to avoid. I am quite new to this, I am probably making mistakes, so I would like to read them.

"use client";

import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
import { useEffect, useState } from "react";

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    setIsLoaded(true);
  }, []);

  if (!isLoaded) {
    return null;
  }

  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

riberojuanca avatar Oct 25 '24 13:10 riberojuanca

'use client'

import * as React from 'react'
const NextThemesProvider = dynamic(
	() => import('next-themes').then((e) => e.ThemeProvider),
	{
		ssr: false,
	}
)

import { type ThemeProviderProps } from 'next-themes/dist/types'
import dynamic from 'next/dynamic'

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
	return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

i tried that it works fine, but when i am using next intl, with this fix, i got non sense bug. assume i have 2 languages (en, de) if i have "de" selected and refresh the page it's fine, but if i switch to "en" and switch back to "de" i only see blank white screen. this not happening if i refresh the page with the default language in my case "en". my best guess next intl is changing the html props to add language like this

const { locale } = await params;

  return (
    <html lang={locale}>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <MainProvider>{children}</MainProvider>
      </body>
    </html>
  );

and next themes is changing over it, or vise versa. that introduce this conflict, i checked the terminal and the pages was requested exactly as it supposed to no errors on console, no errors on terminal

abdelhamied403 avatar Oct 30 '24 09:10 abdelhamied403

if (!isLoaded) {
    return null;
  }

that's not a good solution either. That useEffect will not run until after the component is mounted and painted, meaning you will initially see a blank screen for hundred or so milliseconds before NextThemesProvider can run. It's not an ideal. The whole point of SSR is to avoid this type of rendering delay. According to next-theme's readme the suppressHydrationWarning prop on the html tag is mostly innocuous because

"This property [suppressHydrationWarning] only applies one level deep, so it won't block hydration warnings on other elements."

kamyarkaviani avatar Nov 02 '24 02:11 kamyarkaviani

Hey, as it is marked on the doc, you should add suppressHydrationWarning on your html tag :

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <>
      <html lang="en" suppressHydrationWarning>
        <head />
        <body>
          <ThemeProvider
            attribute="class"
            defaultTheme="system"
            enableSystem
            disableTransitionOnChange
          >
            {children}
          </ThemeProvider>
        </body>
      </html>
    </>
  )
}

This worked for me in Next.js 15

aliawanai avatar Nov 03 '24 18:11 aliawanai

The suppressHydrationWarning won't work for deeper server-client differences like a Theme switcher:

<div className="flex flex-col items-center space-y-2">
  <Label>Theme</Label>
  <RadioGroup
    value={theme}
    onValueChange={setTheme}
    className="flex justify-center gap-4"
  >
    <div className="flex items-center space-x-2">
      <RadioGroupItem value="dark" id="dark" />
      <Label htmlFor="dark">Dark</Label>
    </div>
    <div className="flex items-center space-x-2">
      <RadioGroupItem value="light" id="light" />
      <Label htmlFor="light">Light</Label>
    </div>
  </RadioGroup>
</div>

Disabling ssr will lead to a flash which is not a fix but rather a reshaped issue.

Does anyone know why the current theme is not available when ssr?

yannickschuchmann avatar Nov 03 '24 21:11 yannickschuchmann

Hey, as it is marked on the doc, you should add suppressHydrationWarning on your html tag :

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <>
      <html lang="en" suppressHydrationWarning>
        <head />
        <body>
          <ThemeProvider
            attribute="class"
            defaultTheme="system"
            enableSystem
            disableTransitionOnChange
          >
            {children}
          </ThemeProvider>
        </body>
      </html>
    </>
  )
}

Thank you!

So this is not connected to Next.js 15? It's still that old problem we had with ThemeProvider?

yeah this solves my problem

sallah26 avatar Nov 06 '24 07:11 sallah26

I have the same issue on Next Js 14

hcdiekmann avatar Nov 07 '24 07:11 hcdiekmann

I have the same issue on Next Js 14

Have you tried the suggestion provided by @Medaillek ?

aliawanai avatar Nov 13 '24 08:11 aliawanai

okay for me there was dependency issue for farmer-motion with next js 15 - react 19

├─┬ framer-motion 11.11.13
│ ├── ✕ unmet peer react@^18.0.0: found 19.0.0-rc-66855b96-20241106
│ └── ✕ unmet peer react-dom@^18.0.0: found 19.0.0-rc-66855b96-20241106
└─┬ next-themes 0.3.0
  ├── ✕ unmet peer react@"^16.8 || ^17 || ^18": found 19.0.0-rc-66855b96-20241106
  └── ✕ unmet peer react-dom@"^16.8 || ^17 || ^18": found 19.0.0-rc-66855b96-20241106 

also weirdly could not find the ThemeProviderProps

import { type ThemeProviderProps } from "next-themes/dist/types";

So, I downgrade "next-themes": "^0.4.3", to "next-themes": "^0.3.0",

no hydration error now !!! happy 😁

mehdad-hussain avatar Nov 13 '24 10:11 mehdad-hussain

Hey, as it is marked on the doc, you should add suppressHydrationWarning on your html tag :

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <>
      <html lang="en" suppressHydrationWarning>
        <head />
        <body>
          <ThemeProvider
            attribute="class"
            defaultTheme="system"
            enableSystem
            disableTransitionOnChange
          >
            {children}
          </ThemeProvider>
        </body>
      </html>
    </>
  )
}

Thank you! So this is not connected to Next.js 15? It's still that old problem we had with ThemeProvider?

Here is the real fix :

'use client'

import * as React from 'react'
const NextThemesProvider = dynamic(
	() => import('next-themes').then((e) => e.ThemeProvider),
	{
		ssr: false,
	}
)

import { type ThemeProviderProps } from 'next-themes/dist/types'
import dynamic from 'next/dynamic'

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
	return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

You have to dynamically import the theme provider from next-theme and you can now remove the suppressHydrationWarning from your <html> tag

It did fix the hydration error for me but it caused this:

--><template
      data-dgst="BAILOUT_TO_CLIENT_SIDE_RENDERING"
      data-msg="Switched to client rendering because the server rendering errored:

Bail out to client-side rendering: next/dynamic"
      data-stck="Switched to client rendering because the server rendering errored:

Error: Bail out to client-side rendering: next/dynamic
    at BailoutToCSR (webpack-internal:///(ssr)/./node_modules/next/dist/shared/lib/lazy-dynamic/dynamic-bailout-to-csr.js:15:15)
    at react-stack-bottom-frame (

Not sure how will this affect the SEO of my page?

SyahmiRafsan avatar Nov 15 '24 10:11 SyahmiRafsan

For anyone who doesn’t want to scroll through all the text above: Here’s what I’ve found and confirmed—among all the solutions, adding suppressHydrationWarning to your HTML tag is the best solution for now!

Solution 1:

As it is marked on the next-themes Github Readme, you should add suppressHydrationWarning on your html tag :

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <>
      <html lang="en" suppressHydrationWarning>
        <head />
        <body>
          <ThemeProvider
            attribute="class"
            defaultTheme="system"
            enableSystem
            disableTransitionOnChange
          >
            {children}
          </ThemeProvider>
        </body>
      </html>
    </>
  )
}

This property only applies one level deep, so it won't block hydration warnings on other elements.

Solution 2:

Less effective Solution: You need to dynamically import the ThemeProvider from next-theme, which allows you to remove the suppressHydrationWarning from your tag.

'use client'

import * as React from 'react'
const NextThemesProvider = dynamic(
	() => import('next-themes').then((e) => e.ThemeProvider),
	{
		ssr: false,
	}
)

import { type ThemeProviderProps } from 'next-themes/dist/types'
import dynamic from 'next/dynamic'

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
	return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

This solution effectively prevents errors during SSR but isn’t an "elegant" approach; but dynamically loading the next-themes library can result in undesired outcomes, such as displaying a "snapshot of the wrong theme" initially.

Verdict: It’s best to stick with Solution 1 for now until a proper solution is released.

devdatkumar avatar Nov 26 '24 16:11 devdatkumar

With solution 1, I'm getting this errors

Cannot find module '@/components/theme-provider' or its corresponding type declarations.ts(2307)

Cannot find name 'RootLayoutProps'.ts(2304)

Any suggestions???

Thanks

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <>
      <html lang="en" suppressHydrationWarning>
        <head />
        <body>
          <ThemeProvider
            attribute="class"
            defaultTheme="system"
            enableSystem
            disableTransitionOnChange
          >
            {children}
          </ThemeProvider>
        </body>
      </html>
    </>
  )
}

birarr avatar Dec 02 '24 09:12 birarr

@birarr follow this guide, you probably don't have the theme-provider file.

devdatkumar avatar Dec 02 '24 10:12 devdatkumar

@birarr follow this guide, you probably don't have the theme-provider file.

Actually, I do have this on my project already. But I was able to fix it now, adding the "suppressHydrationWarning" on my html tag.

birarr avatar Dec 02 '24 10:12 birarr

Hey, as it is marked on the doc, you should add suppressHydrationWarning on your html tag :

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <>
      <html lang="en" suppressHydrationWarning>
        <head />
        <body>
          <ThemeProvider
            attribute="class"
            defaultTheme="system"
            enableSystem
            disableTransitionOnChange
          >
            {children}
          </ThemeProvider>
        </body>
      </html>
    </>
  )
}

Thank you! So this is not connected to Next.js 15? It's still that old problem we had with ThemeProvider?

Here is the real fix :

'use client'

import * as React from 'react'
const NextThemesProvider = dynamic(
	() => import('next-themes').then((e) => e.ThemeProvider),
	{
		ssr: false,
	}
)

import { type ThemeProviderProps } from 'next-themes/dist/types'
import dynamic from 'next/dynamic'

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
	return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

You have to dynamically import the theme provider from next-theme and you can now remove the suppressHydrationWarning from your <html> tag

I was facing this problem too and this works well. Thank you @SyahmiRafsan

melodyxpot avatar Dec 03 '24 09:12 melodyxpot

Hey, as it is marked on the doc, you should add suppressHydrationWarning on your html tag :

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <>
      <html lang="en" suppressHydrationWarning>
        <head />
        <body>
          <ThemeProvider
            attribute="class"
            defaultTheme="system"
            enableSystem
            disableTransitionOnChange
          >
            {children}
          </ThemeProvider>
        </body>
      </html>
    </>
  )
}

Thank you! So this is not connected to Next.js 15? It's still that old problem we had with ThemeProvider?

Here is the real fix :

'use client'

import * as React from 'react'
const NextThemesProvider = dynamic(
	() => import('next-themes').then((e) => e.ThemeProvider),
	{
		ssr: false,
	}
)

import { type ThemeProviderProps } from 'next-themes/dist/types'
import dynamic from 'next/dynamic'

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
	return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

You have to dynamically import the theme provider from next-theme and you can now remove the suppressHydrationWarning from your <html> tag

I was facing this problem too and this works well. Thank you @SyahmiRafsan

But it blinking on reload the page...(

uigywnkiub avatar Dec 18 '24 16:12 uigywnkiub

Still facing with this issue. Any one have better solutions?

suppressHydrationWarning is work but I think this is not a real fix.

adding dynamic logic in themeProvider is also working but make another issue.

:(

just for information:

chakra-ui suggest add suppressHydrationWarning option at html tag in doc.

https://www.chakra-ui.com/docs/get-started/frameworks/next-app

  "dependencies": {
   "class-variance-authority": "^0.7.1",
   "clsx": "^2.1.1",
   "lucide-react": "^0.469.0",
   "next": "15.1.2",
   "next-themes": "^0.4.4",
   "react": "^19.0.0",
   "react-dom": "^19.0.0",
   "tailwind-merge": "^2.5.5",
   "tailwindcss-animate": "^1.0.7"
 },
 "devDependencies": {
   "@eslint/eslintrc": "^3",
   "@types/node": "^20",
   "@types/react": "^19.0.2",
   "@types/react-dom": "^19",
   "eslint": "^9",
   "eslint-config-next": "15.1.2",
   "postcss": "^8",
   "tailwindcss": "^3.4.1",
   "typescript": "^5"
 }
}

gwongibeom avatar Dec 21 '24 15:12 gwongibeom

I'm getting the same error too. I am looking for a real solution, I think I will continue with the "suppressHydrationWarning" solution for now. If anyone has a better solution, you would be happy to see the answer.

BerkCertel avatar Dec 23 '24 15:12 BerkCertel

I've been sitting with this problem for two days. The solution to add suppressHydrationWarning to html helped. Thanks guys. I didn't know what to do anymore

ktoYaTako avatar Dec 26 '24 18:12 ktoYaTako

hi yall

for some reason suppressHydraitonWarning isn't working for me..

import { ThemeProvider } from "@/components/theme-provider";]

export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <ThemeProvider
        attribute="class"
        defaultTheme="system"
        enableSystem
        disableTransitionOnChange
      >
        <TonConnectProvider>
          <body className='flex flex-col min-h-[99vh] antialiased'>
            <main className="flex-grow">
              {children}
            </main>
          </body>
        </TonConnectProvider>
      </ThemeProvider>
    </html>
  );
}
'use client';

import { ThemeProvider as NextThemesProvider } from "next-themes";
import React from "react";

export function ThemeProvider({ children, ...props }: React.ComponentProps<typeof NextThemesProvider>) {
    return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

any thoughts?

Im trying to avoid disabling ssr, cause it makes a little flashbang on load, and thats not what i really want to see.

I would be grateful for any help.

jus1d avatar Jan 09 '25 09:01 jus1d

https://github.com/shadcn-ui/ui/issues/5552#issuecomment-2437836976

@jus1d, you might want to give that a try. It worked for me like a charm. good luck

rafadlis avatar Jan 14 '25 15:01 rafadlis

@rafadlis thanks, but dynamic imports creating a new issue with a little flash bang on load, which is very annoying.

Though, suppressHydrationError will not work in this case, because it suppress only text difference errors or something like that. I personally wrote a little context with my own hooks and that works perfect

jus1d avatar Jan 14 '25 15:01 jus1d

Thank you guys it worked with adding suppressHydrationError on the HTML tag

Adnan-Jemal avatar Jan 25 '25 11:01 Adnan-Jemal

None of the solutions discussed here have worked for me. I'm using next: 15.1.3 , react: ^19.0.0 , node: v22.12.0

Image

  <ClientSegmentRoot Component={function DashboardLayout} slots={{...}} params={{}}>
                                            <DashboardLayout params={Promise}>
                                              <html lang="en" suppressHydrationWarning={true}>
                                                <head>
                                                  <body
                                                    >
                                                      ...
                                                        ...
                               className="dark"
                                style={{color-scheme:"dark"}}
                                      data-new-gr-c-s-check-loaded="14.1092.0"
                                      data-gr-ext-installed=""
                                                    className="geist_6feb203d-module__8DQF1a__variable geist_mono_c7d183a-module__ZW1U..."
                                                    data-new-gr-c-s-check-loaded="14.1092.0"
                                                    data-gr-ext-installed=""

Mahad871 avatar Feb 02 '25 20:02 Mahad871