Like some many other answers stated, e.g. this one, Next.js runs both on the client as well as on the server, so you need a guard to be able to properly fetch from localStorage
:
if (typeof localStorage !== "undefined") {
return localStorage.getItem("theme")
} else {
return "light"
}
That’s what I’m doing as well, and, since I’m using DaisyUI and having my theme specified on <html data-theme={theme}>
, I’m also basically wrapping my whole app with a theme provider.
This has yielded me 2 problems:
- The app mounts with the default theme for a very brief moment, then identifies what’s in
localStorage
, and goes into the saved, expected theme. - The icon button carrying the respective icon for the theme does not sync properly initially.
If I take off the the guard, then everything actually works as expected, and none of the problems mentioned above, but then I get this error on the server:
⨯ src/lib/context/ThemeContext.tsx (37:10) @ getPreference
⨯ ReferenceError: localStorage is not defined
at getPreference
at ThemeProvider
36 | const storedPreference = localStorage.getItem("theme")
> 37 | return storedPreference
| ^
38 | ? stringToTheme(storedPreference)
39 | : "light"
I think this means I might need to disable SSR for the whole app somehow. Is this the way to go? Or is there another way to go? Maybe there’s a way of disabling SSR only for this somehow?
I’ve tried something like this, and it does work, though I don’t know it’s ideal, after all it disables one of the biggest benefits of Next.js itself:
const DynamicApp = dynamic(
() =>
import("./dapp").then((mod) => mod.ThemedAndAuthedApp),
{
loading: () => (
<html>
<body>
<p>Loading...</p>
</body>
</html>
),
ssr: false,
}
)
I do get this error though:
Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
2
Answers
As suggested by @amirseify and @Zulzidan.com, the standard way of solving this is through cookies. The local storage is for the client only, so you cannot do SSR from it. However, cookies are sent to the server with requests, and are altered by the server itself on the client.
And Next.js comes with a handy cookies API already. I'm also using Shad UI, and it does point out that there is a
next-themes
package we could use for this case, but I wasn't able to do it. Instead, I went with my custom theme context.Given all that, here's a solution I was able to come up with:
Get the theme cookie on the
RootLayout
:Get the themed app into another file. Since we're going to use the theme context, we need to use
"use client"
— this might defeat Next.js's SSR purpose, but it's the best I could, since the whole app needs the theme after all... —:Create a server action for saving the theme into a cookie:
Create a button for cycling the theme through the theme context and saving it on a cookie through the server action:
It’s better to manage the theme preference effectively while maintaining server-side rendering (SSR) capabilities. Because that’s the core usage of NextJs
First initialize the theme state in the context provider with a default theme to avoid visible theme switch on client load
Then make sure
getPreference
function ensures that localStorage is accessed only client-side, preventing server-side errors.Finally use
useEffect
hook to apply the theme after the component mounts, ensuring correct hydration and avoiding UI flicker.ThemeContext.tsx
: