Shadee Merhi
Shadee Merhi

Reputation: 333

React custom hook state change not triggering re-render

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

Answers (1)

chesterkmr
chesterkmr

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

Related Questions