So I’m using nextjs and I’m trying to build a Daisyui theme switcher. It generally works just fine, but there are some kinks I don’t quite understand.
I have a simple theme switch button:
// Themeswitch.jsx
'use client';
import { useContext } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
import { ThemeContext } from 'context/SiteTheme';
const ThemeSwitch = function () {
const { theme, setTheme, choices, darkMode } = useContext(ThemeContext);
return (
<button
type="button"
className="btn btn-ghost"
onClick={() => {
setTheme(theme === choices[0] ? choices[1] : choices[0]);
}}
aria-label="switch theme between light and dark"
>
<FontAwesomeIcon icon={darkMode ? faSun : faMoon} />
</button>
);
};
export default ThemeSwitch;
As you can see, it’s a client component. It still gets rendered on the server side, and If I log out the theme and dark mode, then on the server side, it logs out as "dim" and true and "emerald" and false on the client side, as it picks up the theme name from the local storage. But the thing is – the icon does not switch from "sun" to "moon" when rendering on the client side. Why?
And on a related note, this is the context provider looks like this:
'use client';
import { createContext, useMemo, useState, useEffect } from 'react';
export const ThemeContext = createContext();
const isLocalStorageEnabled = () => {
try {
const key = `__storage__test`;
window.localStorage.setItem(key, null);
window.localStorage.removeItem(key);
return true;
} catch (e) {
return false;
}
};
export const ThemeContextProvider = function ({ children, defaultTheme, choices }) {
// const localTheme = isLocalStorageEnabled() && localStorage.theme;
const localTheme = localStorage.theme;
let baseTheme;
if (choices.indexOf(localTheme) > -1) {
baseTheme = localTheme;
} else {
baseTheme = defaultTheme;
}
const [theme, setTheme] = useState(baseTheme); // Replace null with your initial theme state
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.theme = theme;
}, [theme]);
const memoizedContext = useMemo(
() => ({ theme, setTheme, choices, darkMode: theme === defaultTheme }),
[choices, defaultTheme, theme],
);
return <ThemeContext.Provider value={memoizedContext}>{children}</ThemeContext.Provider>;
};
If I run the code like this, then localStorage.theme generates an error on the server side, obviously, as it’s not available. However, having it generate an error on the server side is the only way to ensure that the theme does not visibly switch/flicker when rendered on the client side. If I comment in the line const localTheme = isLocalStorageEnabled() && localStorage.theme;
, then there’s no error on the server side, but then the page first renders as darktheme, as that’s default and then switches to the light theme ( if it’s turned on) about 0.5 seconds later. Also, if I have an error on the server side, then the icon also renders correctly.
Is there a way to do that better?
2
Answers
The issue you’re facing is due to the hydration process in Next.js. When the server renders the component, it uses the default theme because
localStorage
is not available on the server-side. However, when the client-side JavaScript kicks in, it reads the theme fromlocalStorage
and updates the state accordingly, causing a mismatch between the server-rendered output and the client-side state.To fix this,
Let’s set a use
mounted
state to track the component’s mount status.Then on the initial render, mounted
mounted
is set tofalse
, so the provider does not render theThemeContext.Provider
. This prevent any client-side code from executing during server-side rendering.Let’s use a
useEffect
hook to check the theme inlocalStorage
and set thetheme
state accordingly. Let make sure, this hook only runs in client side by checking the availability of thelocalStorage
. We keep the dependency array empty to make sure it runs only on initial render. Also after thetheme
is set, themounted
state should set totrue
According to the logic we set on above using
mounted
state, The provider now renders theThemeContext.Provider
with the correct theme state, ensuring that the client-side and server-side outputs match.Then finally we use the existing
useEffect
hook to handle updating thedata-theme
attribute andlocalStorage
whenever thetheme
state changes.Here’s the completed code.
i don’t know if it is a good idea , but to prevent the website from flickering, you may save the theme status in the server (db from example) where you can access the value on server side , maybe the session as well where you can access it on the server also