I have next error. I have two variants of theme in my app - dark & light.
ThemeContext.ts
export const ThemeContext = createContext<{
theme: AppThemeInterface,
setTheme: Dispatch<SetStateAction<AppThemeInterface>>,
// eslint-disable-next-line no-unused-vars
updateThemeKey: (newThemeKey: ThemeKeys) => void,
}>({
theme: getTheme(ThemeKeys.DARK) // here is just object with props from interface,
setTheme: () => null,
updateThemeKey: () => null,
});
AppThemeProvider.ts Here I receive theme_key from LocalStorage and get theme by that key and set it value to ThemeContext
export const AppThemeProvider = ({ children }: AppThemeProviderProps) => {
const [currentThemeKey, setCurrentThemeKey] = useLocalStorage<ThemeKeys>('theme_key', ThemeKeys.DARK);
const [theme, setTheme] = useState<AppThemeInterface>(getTheme(currentThemeKey));
const updateThemeKey = (value: ThemeKeys) => {
setCurrentThemeKey(value);
};
return (
<ThemeContext.Provider value={
{ theme, setTheme, updateThemeKey }
}>
<ThemeProvider theme={theme}>
{children}
</ThemeProvider>
</ThemeContext.Provider>
);
};
useLocalStorageHook
import { useState } from 'react';
export const useLocalStorage = <T>(key: string, initialValue: T) => {
const [value, setValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue;
}
const lcItem = localStorage.getItem(key);
const endVal = lcItem ? JSON.parse(lcItem) : initialValue;
localStorage.setItem(key, JSON.stringify(endVal));
return endVal;
});
const saveValue = (value: T): void => {
localStorage.setItem(key, JSON.stringify(value));
};
const onChangeValue = (value: T): void => {
saveValue(value);
setValue(value);
};
return [value, onChangeValue] as const;
};
With such setup, when key in LocalStorage differ from dark, so in case in LocalStorage I have light i have next error
**
- Prop
className
did not match. Server:
**
That is because first it's rendered as dark theme, but then it rerenders with light theme.
I don't understand how to make it render properly
CodePudding user response:
export const ThemeContext = createContext<{
theme: AppThemeInterface,
setTheme: Dispatch<SetStateAction<AppThemeInterface>>,
// eslint-disable-next-line no-unused-vars
updateThemeKey: (newThemeKey: ThemeKeys) => void,
}>({
theme: getTheme(ThemeKeys.DARK) // here is just object with props from interface,
setTheme: () => null,
updateThemeKey: () => null,
});
CodePudding user response:
Problem
The state value
will always be calculated on the server using the initialValue
, however, when it's being calculated on the client it's using the key in localStorage (since window
is defined on the client). For example, if the initialValue
is "light" on the server, but may be "dark" in localStorage, you're going to get a hydration error because the value
in state won't match.
Solution
Since localStorage is client-side only, you will need to calculate the theme value in a useEffect. In short, useEffect only runs on the client, therefore you'll always use the initialValue
for both server and client, but on the client only, it'll be recalculated once the useEffect runs in the browser.
Demo
Here's a very simple demo that derives a theme based upon a value in localStorage.
Drawbacks
Using a client-side stored value will result in a UI flash if the value is anything other than the initialValue
. You could avoid this flash by moving the value to an outside resource that can be accessed server-side. For example, saving this value to a database that can be accessed by using one of the Next lifecycle methods: getServerSideProps or getInitialProps. But this brings up another obstacle: How do you know which value in the database belongs to the current user? Regardless of whether a value is retrieved from the client or server, you're going to run into hurdles when it's used to dynamically set a global value.