Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

I am using nextjs 13, typescript and tailwind and get this error of hydration #169

Open
riteshreg opened this issue Mar 24, 2023 · 23 comments

Comments

@riteshreg
Copy link

This error is not in the console in deployment version but shows in localhost.
In code when I comment the ThemeProvider the error is gone

react_devtools_backend.js:2655 Warning: Extra attributes from the server: class,style
at html
at ScrollAndFocusHandler (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/layout-router.js:153:1)
at InnerLayoutRouter (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/layout-router.js:195:11)
at RedirectErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/layout-router.js:361:9)
at RedirectBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/layout-router.js:368:11)
at NotFoundBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/layout-router.js:404:11)
at LoadingBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/layout-router.js:317:11)
at ErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/error-boundary.js:72:11)
at RenderFromTemplateContext (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/render-from-template-context.js:12:34)
at OuterLayoutRouter (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/layout-router.js:23:11)
at ReactDevOverlay (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:61:9)
at HotReload (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:20:11)
at Router (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/app-router.js:48:11)
at ErrorBoundaryHandler (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/error-boundary.js:59:9)
at ErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/error-boundary.js:72:11)
at AppRouter (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/app-router.js:24:13)
at ServerRoot (webpack-internal:///(app-client)/./node_modules/next/dist/client/app-index.js:147:11)
at RSCComponent
at Root (webpack-internal:///(app-client)/./node_modules/next/dist/client/app-index.js:164:11)

@SeyyedKhandon
Copy link

SeyyedKhandon commented Apr 4, 2023

You just need to make sure the component is loaded on client side using an useEffect + isMounted useState:
more info

import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'

const ThemeSwitch = () => {
  const [mounted, setMounted] = useState(false)
  const { theme, setTheme } = useTheme()

  // useEffect only runs on the client, so now we can safely show the UI
  useEffect(() => {
    setMounted(true)
  }, [])

  if (!mounted) {
    return null
  }

  return (
    <select value={theme} onChange={e => setTheme(e.target.value)}>
      <option value="system">System</option>
      <option value="dark">Dark</option>
      <option value="light">Light</option>
    </select>
  )
}

export default ThemeSwitch

Or you can use a simple version from this pull request updated document

@Jasonkoolman
Copy link

Jasonkoolman commented Apr 16, 2023

ThemeProvider does two things (both on the server as on the client):

  1. Adds a class to <html>
  2. Adds a style attribute colorScheme to <html>

The problem is that the HTML that is rendered on the server is not the same as the HTML rendered on the client. Hence you see the warning Warning: Extra attributes from the server: class,style.

I'm using Next 13 with the new app directory and the hydration error is resolved by applying the class and style attributes:

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <html
      lang="en"
      style={{ colorScheme: 'light' }} // <--
      className={classNames(fontSans.variable, 'light')} // <--
    >
      <body className="min-h-screen">
        <ThemeProvider attribute="class" defaultTheme="light">
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

However, the real problem is when user preferences change. Next Theme is storing the preferred theme in localStorage, which will not work as the server is not aware of the preference. Also see vercel/next.js#21982.

A possible solution is to use cookies or store the preference to a database (which you can fetch server-side).

Edit
It is possible to suppress the hydration warning by applying the prop suppressHydrationWarning to <html> (it's all in the name). See #152 (comment)

@Jafetlch
Copy link

That works momentarily, but yes, the problem comes when the user wants to choose the theme, hopefully this hydratation issue will be solved without having to use suppressHydrationWarning

@statusunknown418
Copy link

ThemeProvider does two things (both on the server as on the client):

  1. Adds a class to <html>
  2. Adds a style attribute colorScheme to <html>

The problem is that the HTML that is rendered on the server is not the same as the HTML rendered on the client. Hence you see the warning Warning: Extra attributes from the server: class,style.

I'm using Next 13 with the new app directory and the hydration error is resolved by applying the class and style attributes:

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <html
      lang="en"
      style={{ colorScheme: 'light' }} // <--
      className={classNames(fontSans.variable, 'light')} // <--
    >
      <body className="min-h-screen">
        <ThemeProvider attribute="class" defaultTheme="light">
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

However, the real problem is when user preferences change. Next Theme is storing the preferred theme in localStorage, which will not work as the server is not aware of the preference. Also see vercel/next.js#21982.

A possible solution is to use cookies or store the preference to a database (which you can fetch server-side).

Edit It is possible to suppress the hydration warning by applying the prop suppressHydrationWarning to <html> (it's all in the name). See #152 (comment)

hard coding style and className will result in another error if the user tries to change the default theme to light or dark for example

@narr07
Copy link

narr07 commented May 9, 2023

This work for me after folowing this #152 (comment)
my setup

"next": "13.4.1",
"next-themes": "^0.2.1",
"tailwindcss": "3.3.2",

Providers.tsx

"use client";

import { ThemeProvider } from "next-themes";
import { useEffect, useState } from "react";

function Providers({ children }: { children: React.ReactNode }) {
  const [mounted, setMounted] = useState(false);

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

  if (!mounted) {
    return null;
  }
  return (
    <ThemeProvider attribute="class" enableSystem={true}>
      {children}
    </ThemeProvider>
  );
}

export default Providers;

layout.tsx

import "../globals.css";
import { Inter} from "next/font/google";
import Header from "@/components/Header";
import Providers from "../Providers";


const sg = Inter({ subsets: ["latin"], display: "swap" });

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="id">
      <Providers>
        <body className={sg.className}>
          <Header />
          <div className="pt-16">{children}</div>
        </body>
      </Providers>
    </html>
  );
}

DarkToggle.tsx

"use client"

import React, { useState, useEffect } from "react";
import { useTheme } from "next-themes";
import { RiSunLine, RiMoonClearLine } from "react-icons/ri";

function DarkToggle() {
  const [mounted, setMounted] = useState(false);
  const [isDark, setIsDark] = useState(false);
  const { theme, setTheme } = useTheme();

  useEffect(() => {
    setMounted(true);
    setIsDark(theme === "dark");
  }, [theme]);

  if (!mounted) {
    return null;
  }

  const toggleDarkMode = () => {
    setIsDark(!isDark);
    setTheme(theme === "light" ? "dark" : "light");
  };

  return (
    <button onClick={toggleDarkMode} className="flex items-center focus:outline-none"
    >
      {isDark ? (
        <div className="mr-2 text-2xl text-yellow-400"><RiSunLine /></div>
      ) : (
        <div className="mr-2 text-2xl text-gray-700">
          <span className="text-yellow-400"><RiMoonClearLine /></span>
        </div>
      )}
    </button>
  );
}

export default DarkToggle;

@fubhy
Copy link

fubhy commented May 11, 2023

This work for me after folowing this #152 (comment) my setup

You absolutely don't want to do that. By doing that, you are essentially completely eliminating server side rendering. So this doesn't solve the problem, it just avoids it by dropping the ball on all the good stuff :-P

@lundjrl
Copy link

lundjrl commented May 25, 2023

SSR works for me via the fork mentioned here

@dsidler88
Copy link

dsidler88 commented Jun 1, 2023

Returning null until mounted is not an acceptable solution, it results in a noticeable delay in rendering, and looks clunky. I have yet to find a solution for a Theme toggle button that actually functions as it should.

What I have below actually works perfectly on the client: I initially set a separate variable "localTheme" using localStorage. After the initial render I go back to using next-themes "setTheme" to toggle.

However, this works fine on the client but throws an error on the server because it has no "window" object. I am not sure why it's trying to run anything on the server, as I've marked this with "use client".

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

  const localTheme = localStorage.theme;

  return (
    <div>
      {localTheme === "dark" ? (
        <button
          onClick={() => {
            setTheme("light");
          }}
          className="bg-slate-100 p-2 rounded-xl"
        >
``` ...

Anyway, my current options seem to be ignoring constant errors server side, or an extremely clunky wait for it to be mounted.  I appreciate any further suggestions

@MafinBoy
Copy link

MafinBoy commented Jun 14, 2023

In NextJS 13 it is possible for the server to read user cookies natively, if this library was modified so it's using cookies instead of local storage this would fix the hydration problem completely.
It would however break compatibility with versions of NextJS before 13.
I made a working proof of concept here.
@pacocoursey would you be interested in merging my PR if I remade next-themes to use cookies instead of local storage?

@joepetrillo
Copy link

Any updates on this? I still get the error :/

@dsidler88
Copy link

Not a fix for the hydration error, but if you are just doing this “isMounted” workaround to implement dark mode, there is an easy alternative.

just use TailwindCSS classes, which use CSS media queries behind the scenes. “dark:border-white” for example. If you are using different components, or different background images, use “dark:hidden” and easily manipulate what is displayed without waiting for components to mount.

@joepetrillo
Copy link

Not a fix for the hydration error, but if you are just doing this “isMounted” workaround to implement dark mode, there is an easy alternative.

just use TailwindCSS classes, which use CSS media queries behind the scenes. “dark:border-white” for example. If you are using different components, or different background images, use “dark:hidden” and easily manipulate what is displayed without waiting for components to mount.

Hmm, I am using tailwind, but I am not sure what you mean exactly. The html tag is not getting its class and style attributes set until ThemeProvider mounts. Therefore, in theory with tailwind, the theme of everything would be light mode before hydration (since class="dark" is missing momentarily)

My best guess from what others have said is that we need to have the server involved by setting cookies to the users theme preference (and somehow to their system preference by default).

@O4epegb
Copy link

O4epegb commented Jul 3, 2023

In NextJS 13 it is possible for the server to read user cookies natively, if this library was modified so it's using cookies instead of local storage this would fix the hydration problem completely. It would however break compatibility with versions of NextJS before 13. I made a working proof of concept here. @pacocoursey would you be interested in merging my PR if I remade next-themes to use cookies instead of local storage?

Using a cookies call opts a route into dynamic rendering. And if you use it in the main layout file then the whole app becomes dynamic. It was always possible before too, it's not a Next.js 13 thing even. So you prototype is fine, but only for cases when you don't care about static rendering. And honestly you don't even need a library to do that, just call cookie, set a class on html/body, you are done. But this library exists because it's not that trivial to add a theme to a static page without unwanted flash.

@0xboga
Copy link

0xboga commented Jul 21, 2023

ThemeProvider does two things (both on the server as on the client):

  1. Adds a class to <html>
  2. Adds a style attribute colorScheme to <html>

The problem is that the HTML that is rendered on the server is not the same as the HTML rendered on the client. Hence you see the warning Warning: Extra attributes from the server: class,style.

I'm using Next 13 with the new app directory and the hydration error is resolved by applying the class and style attributes:

export default function RootLayout({ children }: RootLayoutProps) {
  return (
    <html
      lang="en"
      style={{ colorScheme: 'light' }} // <--
      className={classNames(fontSans.variable, 'light')} // <--
    >
      <body className="min-h-screen">
        <ThemeProvider attribute="class" defaultTheme="light">
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

However, the real problem is when user preferences change. Next Theme is storing the preferred theme in localStorage, which will not work as the server is not aware of the preference. Also see vercel/next.js#21982.

A possible solution is to use cookies or store the preference to a database (which you can fetch server-side).

Edit It is possible to suppress the hydration warning by applying the prop suppressHydrationWarning to <html> (it's all in the name). See #152 (comment)

Worked as a charm. Thanks.

@kerden97
Copy link

kerden97 commented Sep 25, 2023

After spending a few days with search and trial and error, I managed to resolve the hydration mismatch error in my Next.js 13 application. While it seems to resolve the issue effectively, there may be potential drawbacks that I'm currently unaware of. Here's the strategy I employed, :

Set a Default Theme on First Visit: To avoid inconsistencies between server-rendered and client-rendered content, I set a default theme for all first-time visitors. This means I bypassed checking the system's theme preference on the initial load to prevent discrepancies.

Toggle Theme with Next-Themes: For subsequent theme changes after the initial load, I utilized the next-themes package to handle theme toggling, storing the chosen theme in the localStorage.

Syncing Theme Preference with Cookies: When a user toggles the theme, I simultaneously set a cookie containing the current theme preference. This ensures that if the user refreshes the page or returns later, the server can reference this cookie to deliver content with the correct theme applied right from the initial server render.

This approach effectively eliminates the hydration mismatch error while providing a seamless user experience across sessions.

This is my ThemeButton.tsx

"use client";
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { SunIcon, MoonIcon } from "@heroicons/react/24/solid";
const ThemeButton = () => {
  const { resolvedTheme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return null;
  }
  return (
    <button
      aria-label="Toggle Dark Mode"
      type="button"
      className="flex items-center justify-center rounded-lg p-2 transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-700"
      onClick={() => {
        const newTheme = resolvedTheme === "dark" ? "light" : "dark";
        setTheme(newTheme);
        document.cookie = `theme=${newTheme}; path=/`;
      }}
    >
      {resolvedTheme === "dark" ? (
        <SunIcon className="h-5 w-5 text-orange-300" />
      ) : (
        <MoonIcon className="h-5 w-5 text-slate-800" />
      )}
    </button>
  );
};

export default ThemeButton;

This is my provider.tsx

"use client";
import { ReactNode } from "react";
import { ThemeProvider } from "next-themes";

interface ProviderProps {
  children: ReactNode;
}

const Provider: React.FC<ProviderProps> = ({ children }) => {
  return (
    <ThemeProvider attribute="class" storageKey="theme" defaultTheme="dark">
      {children}
    </ThemeProvider>
  );
};

export default Provider;

and finally my layout.tsx in app directory

import "./globals.css";
import { cookies } from "next/headers";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import Provider from "./providers/provider";
import { Header } from "./components/header/Header";
const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Coinrithm.com ",
  description: "Crypto currencies live market data",
};
function getTheme() {
  const cookieStore = cookies();
  const themeCookie = cookieStore.get("theme");
  const theme = themeCookie ? themeCookie.value : "dark";
  return theme;
}
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const theme = getTheme() as string;
  return (
    <html lang="en" className={theme} style={{ colorScheme: theme }}>
      <body className={inter.className}>
        <Provider>
          <Header />
          {children}
          <footer></footer>
        </Provider>
      </body>
    </html>
  );
}

@jamesryan-dev
Copy link

+1 following - is there a fix on the way?

@jamesryan-dev
Copy link

thanks @kerden97 - that's worked a treat

@caposada
Copy link

Yeah it doesn't fix the bug (hopefully sometime in the future this will be fixed), but adding suppressHydrationWarning will solve the pain in the arse console message.

As stated in the React documentation (https://legacy.reactjs.org/docs/dom-elements.html) the suppressHydrationWarning flag: 'only works one level deep', so it won't effect any other hydration warnings you have in your code. I tested this by adding

{new Date().toString()}
below the tag where I have the suppressHydrationWarning attribute and it still gave me a hydration failure as expected.

@Yrobot
Copy link

Yrobot commented Nov 30, 2023

Same warning, which i think the main reason is that there are some differences between the Server generate html and client init html.

Under the next-themes case, i locate the position which cus the waring, it is because the theme from const { theme } = useTheme(); is different between server side and client side html generating.
BUT, this is resonable, because the theme data is stored in the localstorage in browser, and to make sure the html display correctly at the first time, the theme data should be read before the client html generating, and this theme data is not available on server side. So here we are, we have to deal with this by ourself.

Here is my solution, the main logic is use the theme after client mount:

"use client";
export const useMounted = (): boolean => {
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);
  return mounted;
};
"use client";
import { useMounted } from "@/utils/hooks";

function ThemeSwitcher() {
  const { theme: current, setTheme } = useTheme();
  const mounted = useMounted();
  return (
      <div title="Change Theme" className="dropdown dropdown-end">
      // ...
      {themes.map((theme) => (
        <ThemeOption
          theme={theme}
          key={theme}
          setTheme={setTheme}
          isChecked={mounted ? false : (current === theme)}
        />
      ))}
      // ...
    </div>
  );
}

@hugepizza
Copy link

i guess the problem is laying on rendering with data-theme attr on server side is not working, however next-themes is essentially setting a data-theme attr on .
i removed next-themes and using a wrapper with a

to hold data-theme attr, the Extra attributes err is gone

// wrapper
export default function ThemeWrapper({ children }: { children: ReactNode }) {
  const { theme } = useTheme(); // it's not from next-themes
  return (
    <main data-theme={theme}>
      {theme === "light" ? (
        <Toaster />
      ) : (
        <Toaster
          toastOptions={{ style: { background: "#333", color: "#fff" } }}
        />
      )}
      {children}
    </main>
  );
}
// layout
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" className="w-full h-full suppressHydrationWarning">
      <body className="w-full h-full">
        <ThemeContextProvoder>
          <ThemeWrapper>
            <Navbar />
            {children}
          </ThemeWrapper>
        </ThemeContextProvoder>
      </body>
    </html>
  );
}

corysimmons added a commit to corysimmons/shadcn-ui that referenced this issue Jan 8, 2024
Following the current docs always ends up with hydration errors. I used the solution here pacocoursey/next-themes#169 (comment) to address it.

I migrated suppressHydrationWarning to body instead as it fixed a lot of miscellaneous browser extensions from causing an "Extra attributes from the server" error. This solution: vercel/next.js#22388 (reply in thread)
@dizzyjaguar
Copy link

Any update on this? Plans to get this fixed?

@DwennK
Copy link

DwennK commented Feb 12, 2024

Please fix.

@leandromatos
Copy link

The error is gone when I putted the suppressHydrationWarning on <html>.

const RootLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <html lang="en" className={styles.html()} suppressHydrationWarning>
      <body className={styles.body()}>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests