Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

[Bug]: Next.JS 15 - Hydration Failed: -className="dark" -style={{color-scheme:"dark"}} #316

Closed
AChangXD opened this issue Oct 22, 2024 · 17 comments
Labels
bug Something isn't working triage

Comments

@AChangXD
Copy link

AChangXD commented Oct 22, 2024

What happened?

After upgrading to Next.JS 15, I am now encountering a Hydration error. This error was not there in Next.JS 15 or the 15 RC version that I was using.
image

Adding a mounted check for <NextThemesProvider/> removes the hydration error but introduces another error:

image

Version

0.3.0

What browsers are you seeing the problem on?

Safari

@AChangXD AChangXD added bug Something isn't working triage labels Oct 22, 2024
@AChangXD
Copy link
Author

Adding a mounted check to the fixed the hydration issue. Weird that this is only in Next.JS 15 but not in Next.JS 14:

'use client';

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

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  /* -------------------------------------------------------------------------- */
  /*                                   States                                   */
  /* -------------------------------------------------------------------------- */
  const [mounted, setMounted] = useState(false);

  /* -------------------------------------------------------------------------- */
  /*                                 JSX Return                                 */
  /* -------------------------------------------------------------------------- */
  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return <></>;
  }
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

@fab-uleuh
Copy link

fab-uleuh commented Oct 22, 2024

Same issue with Next 15

For information I hadn't the issue with Next-15-RC-1 and 2. Just with the official 15

@devmoatassem
Copy link

I'm also experiencing the same issue with Next.js 15

@uigywnkiub
Copy link

Currently next upgrades to 15.0.1 by default and it's canary, so this bug also catches me.
image

@jonathanwilke
Copy link

Same for me on Next.js 15.0.0 stable

@AChangXD
Copy link
Author

So after some debugging, I came to the conclusion that the issue can be fixed by adding a mounted check to the component that wraps the <NextThemesProvider/> and anywhere that you have your theme toggle.

Adding a mounted check to the fixed the hydration issue. Weird that this is only in Next.JS 15 but not in Next.JS 14:

'use client';

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

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  /* -------------------------------------------------------------------------- */
  /*                                   States                                   */
  /* -------------------------------------------------------------------------- */
  const [mounted, setMounted] = useState(false);

  /* -------------------------------------------------------------------------- */
  /*                                 JSX Return                                 */
  /* -------------------------------------------------------------------------- */
  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return <></>;
  }
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

@jonathanwilke
Copy link

jonathanwilke commented Oct 22, 2024

This can not be the solution as it will not work on SSR and will cause the UI to flash when dark mode is selected.
@AChangXD would you reopen this so that I don't have to create a new issue?

@AChangXD AChangXD reopened this Oct 22, 2024
@AChangXD
Copy link
Author

This can not be the solution as it will not work on SSR and will cause the UI to flash when dark mode is selected. @AChangXD would you reopen this so that I don't have to create a new issue?

Hmm I didn't realize it wouldn't work when dark mode is selected. It doesn't flash when it is set to "system". I've reopened the issue :)

@AChangXD
Copy link
Author

I'm using tailwind, so I did className="dark" on my <html/> tag. For now, the black to white flash for light mode is 100% better than the white to black flash in dark mode. Would love a fix for this @pacocoursey

@Goldziher
Copy link

Goldziher commented Oct 23, 2024

This works for me:

export default function RootLayout({ children }: { children: ReactNode }) {
	return (
		<html lang="en" className="dark" style={{ "colorScheme": "dark" }}>
			<body>...</body>
		</html>
	);
}

Theme toggling also works.

For a better experience, shift the detection of color theme preference from CSS to a theme selector component or another use client component called in the root layout. Example:

"use client";

import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
import { Button } from "gen/ui/button";
import { useTheme } from "next-themes";
import { useEffect } from "react";

export function ThemeToggle() {
	const { setTheme, theme } = useTheme();

	useEffect(() => {
		const isDarkTheme = globalThis.matchMedia('(prefers-color-scheme: dark)').matches

		setTheme(isDarkTheme ? "dark" : "light");
		}, []);

	return (
		<Button
			data-testid="theme-toggle-button"
			variant="ghost"
			className="dark:hover:bg-primary/20"
			onClick={() => {
				setTheme(theme === "light" ? "dark" : "light");
			}}
		>
			<SunIcon data-testid="theme-toggle-sun-icon" className="dark:hidden h-6 w-6" />
			<MoonIcon data-testid="theme-toggle-moon-icon" className="hidden dark:block bg-inherit h-6 w-6" />
			<span data-testid="theme-toggle-sr-text" className="sr-only">
				Toggle theme
			</span>
		</Button>
	);
}

@MiniOcean404
Copy link

Me too

@Milind-Jamnekar
Copy link

For now I added suppressHydrationWarning in my html tag as suggested by nextjs doc until next-theme come up with any fix. this is how you would use in your layout.tsx

<html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
    </body>
</html>

@gfargo
Copy link

gfargo commented Oct 28, 2024

Another solution is to wrap your <ThemeProvider> with a <Suspense>, mentioned here, e.g.

 <Suspense fallback={<div>Loading...</div>}>
    <ThemeProvider attribute="class" defaultTheme="light" enableSystem>
      {children}
    </ThemeProvider>
  </Suspense>

Resolves the error/warning for me without having to add suppressHydrationWarning 💭

@maxwiseman
Copy link

@Goldziher By adding className="dark" style={{ "colorScheme": "dark" }}, you're effectively just suppressing the error. This resolves the error because it happens to match the result of the operations that are run by ThemeProvider. You'll see the error again if you switch your device to light mode.

Also not really sure how adding another client component changes anything. Seems like your component pretty much does what ThemeProvider does already (see below).

const applyTheme = React.useCallback(theme => {
let resolved = theme
if (!resolved) return
// If theme is system, resolve it before setting theme
if (theme === 'system' && enableSystem) {
resolved = getSystemTheme()
}
const name = value ? value[resolved] : resolved
const enable = disableTransitionOnChange ? disableAnimation() : null
const d = document.documentElement
const handleAttribute = (attr: Attribute) => {
if (attr === 'class') {
d.classList.remove(...attrs)
if (name) d.classList.add(name)
} else if (attr.startsWith('data-')) {
if (name) {
d.setAttribute(attr, name)
} else {
d.removeAttribute(attr)
}
}
}
if (Array.isArray(attribute)) attribute.forEach(handleAttribute)
else handleAttribute(attribute)
if (enableColorScheme) {
const fallback = colorSchemes.includes(defaultTheme) ? defaultTheme : null
const colorScheme = colorSchemes.includes(resolved) ? resolved : fallback
// @ts-ignore
d.style.colorScheme = colorScheme
}
enable?.()
}, [])
// Whenever theme or forcedTheme changes, apply it
React.useEffect(() => {
applyTheme(forcedTheme ?? theme)
}, [forcedTheme, theme])

@maxwiseman
Copy link

I'm using tailwind, so I did className="dark" on my <html/> tag. For now, the black to white flash for light mode is 100% better than the white to black flash in dark mode. Would love a fix for this

@AChangXD You created that white flash. NextJS pre-renders client components, but when it does that, it doesn't run useState or useEffect. Therefor, when your ThemeProvider gets pre-rendered, Next will use the default mounted value (false), and render <></>. Next is pre-rendering a blank document which gets hydrated into your app on the client, so until that hydration happens, you're seeing the blank document, which is white by default. As mentioned by @jonathanwilke, this also effectively disables SSR and SSG for your entire app. next-theme will solve this flash if you let it, but by preventing it from rendering at first, you aren't allowing it to.

The hydration error is expected. There won't be a "fix" for it. It's even mentioned in the readme. For whatever reason Next hasn't really been showing the error until now, but the behavior is the same as its always been.

The correct solution is to suppress the hydration warning.

@pacocoursey
Copy link
Owner

Please read the docs: https://github.com/pacocoursey/next-themes?tab=readme-ov-file#with-app

@pacocoursey pacocoursey closed this as not planned Won't fix, can't repro, duplicate, stale Nov 4, 2024
@SanketPathare
Copy link

Currently next upgrades to 15.0.1 by default and it's canary, so this bug also catches me. image

What happened?

After upgrading to Next.JS 15, I am now encountering a Hydration error. This error was not there in Next.JS 15 or the 15 RC version that I was using. image

Adding a mounted check for <NextThemesProvider/> removes the hydration error but introduces another error:

image ### Version 0.3.0

What browsers are you seeing the problem on?

Safari

Problem solution :

import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import NextTheme from "@/Components/NextTheme";
import Navbar from "@/Components/Navbar";
import { ClerkProvider } from "@clerk/nextjs";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <ClerkProvider>
      <html lang="en" suppressHydrationWarning>
        <body>
          <NextTheme>
            <Navbar />
            {children}
          </NextTheme>
        </body>
      </html>
    </ClerkProvider>
  );
}


Add in layout html attribute - "suppressHydrationWarning "

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
bug Something isn't working triage
Projects
None yet
Development

No branches or pull requests