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 badges2 Answers
Reset to default 3I 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条)