javascript - "Warning: Prop `className` did not match" while setting up a theme provider in Next.js - Stack Ov

I'm trying to make a theme provider with the Context API to set the application theme, which is ju

I'm trying to make a theme provider with the Context API to set the application theme, which is just a className on body.

The context it's pretty simple. On the lazy initializer of the theme state, first, I check if the theme is on localStorage. If the theme is not in local storage, then I set the theme based on the user system preference.

'use client'

import { createContext, useEffect, useState } from 'react'
import {
  type ThemeContextData,
  type ThemeProviderProps,
  type Themes
} from './types'
import { darkTheme, lightTheme } from '@/styles/themes.css'

export const ThemeContext = createContext<ThemeContextData>(
  {} as ThemeContextData
)

export function ThemeProvider({
  children,
  className
}: ThemeProviderProps): JSX.Element {
  const [theme, setTheme] = useState<Themes>(() => {
    if (typeof window !== 'undefined') {
      const isThemeOnLocalStorage = window.localStorage.getItem(
        '@matheussartori/theme'
      )

      if (isThemeOnLocalStorage) {
        return isThemeOnLocalStorage as Themes
      }

      const isSystemThemeDark = window.matchMedia(
        '(prefers-color-scheme: dark)'
      ).matches

      if (isSystemThemeDark) {
        return 'dark'
      }
    }

    return 'light'
  })

  useEffect(() => {
    if (typeof window !== 'undefined') {
      window.localStorage.setItem('@matheussartori/theme', theme)
    }
  }, [theme])

  const themeClassNames = {
    light: lightTheme,
    dark: darkTheme
  }

  const classNames = className
    ? `${themeClassNames[theme]} ${className}`
    : themeClassNames[theme]

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <body className={classNames}>{children}</body>
    </ThemeContext.Provider>
  )
}

In my layout.tsx,I got the following code as well:

import { Inter } from 'next/font/google'

import '@/styles/global.css'
import { ThemeProvider } from '@/contexts/themes/ThemeProvider'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'lorem',
  description:
    'lorem'
}

export default function RootLayout({
  children
}: {
  children: React.ReactNode
}): JSX.Element {
  return (
    <html lang="en">
      <ThemeProvider className={`${inter.className}`}>{children}</ThemeProvider>
    </html>
  )
}

The problem that's happening, it's probably because of the server renderization on the ThemeProvider. The error that shows in the browser, is this one:

Warning: Prop className did not match. Server: "themes_lightTheme__1ra6bom9 __className_0ec1f4" Client: "themes_darkTheme__1ra6bom0 __className_0ec1f4"

Since the server doesn't know the window/localStorage, apparently, on the rehydration, the server is saying that the theme must be light, but it is dark.

Is there a better way of doing this approach?

I'm trying to make a theme provider with the Context API to set the application theme, which is just a className on body.

The context it's pretty simple. On the lazy initializer of the theme state, first, I check if the theme is on localStorage. If the theme is not in local storage, then I set the theme based on the user system preference.

'use client'

import { createContext, useEffect, useState } from 'react'
import {
  type ThemeContextData,
  type ThemeProviderProps,
  type Themes
} from './types'
import { darkTheme, lightTheme } from '@/styles/themes.css'

export const ThemeContext = createContext<ThemeContextData>(
  {} as ThemeContextData
)

export function ThemeProvider({
  children,
  className
}: ThemeProviderProps): JSX.Element {
  const [theme, setTheme] = useState<Themes>(() => {
    if (typeof window !== 'undefined') {
      const isThemeOnLocalStorage = window.localStorage.getItem(
        '@matheussartori/theme'
      )

      if (isThemeOnLocalStorage) {
        return isThemeOnLocalStorage as Themes
      }

      const isSystemThemeDark = window.matchMedia(
        '(prefers-color-scheme: dark)'
      ).matches

      if (isSystemThemeDark) {
        return 'dark'
      }
    }

    return 'light'
  })

  useEffect(() => {
    if (typeof window !== 'undefined') {
      window.localStorage.setItem('@matheussartori/theme', theme)
    }
  }, [theme])

  const themeClassNames = {
    light: lightTheme,
    dark: darkTheme
  }

  const classNames = className
    ? `${themeClassNames[theme]} ${className}`
    : themeClassNames[theme]

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <body className={classNames}>{children}</body>
    </ThemeContext.Provider>
  )
}

In my layout.tsx,I got the following code as well:

import { Inter } from 'next/font/google'

import '@/styles/global.css'
import { ThemeProvider } from '@/contexts/themes/ThemeProvider'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'lorem',
  description:
    'lorem'
}

export default function RootLayout({
  children
}: {
  children: React.ReactNode
}): JSX.Element {
  return (
    <html lang="en">
      <ThemeProvider className={`${inter.className}`}>{children}</ThemeProvider>
    </html>
  )
}

The problem that's happening, it's probably because of the server renderization on the ThemeProvider. The error that shows in the browser, is this one:

Warning: Prop className did not match. Server: "themes_lightTheme__1ra6bom9 __className_0ec1f4" Client: "themes_darkTheme__1ra6bom0 __className_0ec1f4"

Since the server doesn't know the window/localStorage, apparently, on the rehydration, the server is saying that the theme must be light, but it is dark.

Is there a better way of doing this approach?

Share Improve this question edited May 6, 2023 at 17:22 Youssouf Oumar 46.6k16 gold badges103 silver badges105 bronze badges asked May 5, 2023 at 19:02 Matheus SartoriMatheus Sartori 1893 silver badges12 bronze badges
Add a ment  | 

2 Answers 2

Reset to default 3

I think the other answer is a very bad practice. You basically remove SSR pletely and if you do then what is the point of using Next at all? Using cookies for the theme is also not very feasible, you would need to manually handle them for every page.

If you want to keep SSR I would remend you to use this package for example https://github./pacocoursey/next-themes, or at least check how it is done there. It works both with old versions of Next.js and Next.js 13.4+. It uses localStorage and requires very minimal setup.

Yes, it uses suppressHydrationWarning but at this moment it's not possible to implement something like that without it.

The initial render should be the same on the client and server. Anytime you violate this rule, you get a hydration error, like in your case, where there will be no localStorage on the server, and it's present on the browser, leading to a different initial state.

You could move the logic of instantiating the theme inside an useEffect:

"use client";

import { createContext, useEffect, useState } from "react";
import { type ThemeContextData, type ThemeProviderProps, type Themes } from "./types";
import { darkTheme, lightTheme } from "@/styles/themes.css";

export const ThemeContext = createContext<ThemeContextData>({} as ThemeContextData);

export function ThemeProvider({ children, className }: ThemeProviderProps): JSX.Element {
  const [theme, setTheme] = useState<Themes | null>(null);

  useEffect(() => {
    const isThemeOnLocalStorage = window.localStorage.getItem("@matheussartori/theme");

    if (isThemeOnLocalStorage) {
      setTheme(isThemeOnLocalStorage);
      return;
    }

    const isSystemThemeDark = window.matchMedia("(prefers-color-scheme: dark)").matches;

    if (isSystemThemeDark) {
      setTheme("dark");
      return;
    }

    setTheme("light");
  }, []);

  useEffect(() => {
    if (theme) {
      window.localStorage.setItem("@matheussartori/theme", theme);
    }
  }, [theme]);

  const themeClassNames = {
    light: lightTheme,
    dark: darkTheme,
  };

  const classNames = className ? `${themeClassNames[theme]} ${className}` : themeClassNames[theme];

  // This is so you don't show anything before the theme is fully set. You could set a loader or something, or even remove the if statement
  if (!theme) {
    return null;
  }

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <body className={classNames}>{children}</body>
    </ThemeContext.Provider>
  );
}

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745254267a4618861.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信