Reputation: 333
I have created a custom react hook that allows users to toggle the color theme of my application (i.e. 'light' or 'dark'), and the state change is not causing the components that call the hook to re-render.
The hook is called useColorTheme
and it's defined as follows:
import { useEffect } from "react";
import useLocalStorage from "./useLocalStorage";
export default function useColorTheme() {
const [colorTheme, setColorTheme] = useLocalStorage("color-theme", "light");
useEffect(() => {
const className = "dark";
const bodyClass = window.document.body.classList;
colorTheme === "dark"
? bodyClass.add(className)
: bodyClass.remove(className);
}, [colorTheme]);
return [colorTheme, setColorTheme];
}
As you can see, this hook calls another hook called useLocalStorage
, which allows the state to persist on refresh
I got this hook from usehooks.com, and it's defined as:
import { useState } from "react";
const PREFIX = "my-app-";
export default function useLocalStorage(key: string, initialValue: string) {
const prefixedKey = PREFIX + key;
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
const item = window.localStorage.getItem(prefixedKey);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
function setValue(value: unknown) {
try {
/**
* Don't fully understand function typing here
*/
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
window.localStorage.setItem(prefixedKey, JSON.stringify(valueToStore));
}
} catch (error) {
console.log(error);
}
}
return [storedValue, setValue];
}
The colorTheme
value is successfully being stored in localStorage
, and the hook works on the initial application load, but I am having issues in components that call the useColorTheme
hook, like in this DrawerContainer
example:
export default function DrawerContainer() {
// calling the hook
const [theme, setTheme] = useColorTheme();
// component does not re-render when ThemeToggle component toggles theme state
return (
<div className="flex items-center justify-between gap-2">
<FiMenu size={30} className="lg:hidden mr-4 cursor-pointer" />
<Image
className="ml-10 mr-10 sm:mr-2 sm:ml-0 cursor-pointer"
src={
theme === "light"
? "/images/fiverr-logo.png"
: "/images/fiverr-logo-white.png"
}
alt="logo"
width={100}
height={100}
/>
{!user && <AuthButtons showUnderSmall />}
<ThemeToggle />
</div>
);
}
When the value of colorTheme
is toggled in some other component in my application (such as my ThemeToggle
component), the changed state is not being picked up in my DrawerContainer
component, which prevents logic that reads from this state from happening.
I've verified that the state is indeed changing in my browser Dev Tools, so why is my DrawerContainer
component not re-rendering?
Thank you very much in advance. Any insight is greatly appreciated.
Upvotes: 1
Views: 1258
Reputation: 321
Wen't thru your code and see the issue.
Your custom hook is independent,that means in every component it has its own state.
So for example you have two components A,B and both using hook.
when you changing theme
using component A
,new state is being enclosed inside of component A
and not being passed down to B
or other components that using hook as well.
To solve your issue you have to use Context API and use single state which will be passed down to other components using context.
Check out this https://reactjs.org/docs/context.html
This could be implemented like this(pseudo code):
1.Creating context
const themeContext = React.createContext();
2.Implementing hook which will be reused in components to catch context state.
function useTheme() {
return useContext(themeContext);
}
3.Implementing provider component which should be used at Root level so that can be utilized in all children components.
const {Provider} = themeContext;
function ThemeProvider(props) {
const [theme,setTheme] = useState(() => //grabbing initial theme);
const context = useMemo(() => ({theme, setTheme}), [theme])
return <Provider value={context}>{props.children}</Provider>
}
4.Wrapping components which will utilize your context.
function App() {
return (
<ThemeProvider>
<MyComponent />
</ThemeProvider>
)
}
Hope it helps!
Upvotes: 2