Search code examples
javascriptreactjslocal-storage

How to set the state based on the local storage without overwriting it with the initial state?


I try to get the updated localstorage state called darkmode with useContext but when I console log it in functions inside App.tsx it gives the default value set inside Context.js :

App.tsx:

const { darkmode } = useContext(Context);

React.useEffect(() => {
  console.log(darkmode);
}, []);

Context.js:

import { useState, useEffect, createContext } from "react";

const Context = createContext();

function ContextProvider({ children }) {
  const [darkmode, setDarkmode] = useState(false);

  // Local Storage: setting & getting data
  useEffect(() => {
    const darkmode = JSON.parse(localStorage.getItem("darkmode"));
    if (darkmode) {
      setDarkmode(darkmode);
    }
  }, []);

  useEffect(() => {
    localStorage.setItem("darkmode", JSON.stringify(darkmode));
  }, [darkmode]);

  const toggleDarkmode = () => {
    setDarkmode((prev) => !prev);
  };

  return (
    <Context.Provider
      value={{
        darkmode,
        toggleDarkmode,
      }}
    >
      {children}
    </Context.Provider>
  );
}

export { ContextProvider, Context };

Local storage:

localstorage

The console logs:

console

How can I get the latest updated localstorage state with react context?


Solution

  • Issue

    The problem is caused by the below useEffect and how you are initially setting the state:

    useEffect(() => {
      localStorage.setItem("darkmode", JSON.stringify(darkmode));
    }, [darkmode]);
    

    The above useEffect runs every time darkmode changes, but also on mount. And on mount, darkmode is equal to false, the value given to useState. So the localStorage is set to false, overwriting anything previously registered.

    Solution

    A solution is to change how you are setting the state, so you pick what's in the localStroge if there is something:

    const [darkmode, setDarkmode] = useState(!localStorage.getItem("darkmode") ? false : JSON.parse(localStorage.getItem("darkmode")));
    

    Beyond

    Or, in case you don't wanna access to the local storage in useState, which can cause issues for frameworks that render both on the client and server, like Next.js, you can change your useEffect with one additional state:

    const [firstRender, setFirstRender] = useState(true);
    useEffect(() => {
        // This allows the `useEffect` that fetches data from local storage to run first:
        if(firstRender){
          setFirstRender(false);
          return;
        }
        localStorage.setItem("darkmode", JSON.stringify(darkmode));
    }, [darkmode, firstRender]);
    

    Aside

    Also, to log darkmode changes, consider adding it to the dependency array of useEffect:

    React.useEffect(() => {
      console.log(darkmode);
    }, [darkmode]);