Skip to content
Advertisement

Updating the localStorage is not in sync

When I toggle darkmode it doesn’t update in _app unless I have 2 tabs opened and trigger it in one tab, then the other tab gets updated and darkmode is toggled, but not the tab I pressed the toggle. I use useSettings in both index and _app. I recorded a video so it’s easier to see what’s going on: https://youtu.be/W5du7Y-457Y.

const useLocalStorage = (key: any, page: string, defaultValue: object) => {
  const [value, setValue] = useState<any | null>(defaultValue);

  const onStorageUpdate = () => {
    const stickyValue = localStorage.getItem(key);

    console.log(stickyValue, page);

    setValue(JSON.parse(localStorage.getItem(key)!));
  };

  useLayoutEffect(() => { 
    const stickyValue = localStorage.getItem(key);

    if (localStorage.getItem(key)) { 
      setValue(JSON.parse(localStorage.getItem(key)!)); 
    } else {
      localStorage.setItem(key, JSON.stringify(value));
    }
  }, [key]);
 
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  useEffect(() => {
    addEventListener("storage", onStorageUpdate);
    return () => {
      removeEventListener("storage", onStorageUpdate);
    };
  }, []);

  return [value, setValue, page];      
}

function useSettings(page: string) {
  let [settings, setSettings] = useLocalStorage('settings', page, { darkmode: false, status: true });

  return [settings, setSettings, page];
}

export default useSettings;

Index

import type { NextPage } from 'next'
import IOSToggle from '../components/IOSToggle'
import useSettings from '../components/useSettings'


const Home: NextPage = () => {
  const [settings, setSettings, HandleStorage] = useSettings("index");

  const HandleIosToggle = () => {
    setSettings({ ...settings, darkmode: !settings.darkmode  })
  }

  return (
    <IOSToggle label={"Darkmode"} checked={settings.darkmode} onChange={HandleIosToggle} />
  );
}

export default Home

_app

export default function MyApp(props: MyAppProps) {
  const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
  const [settings, setSettings, page] = useSettings("_app");

  const theme = React.useMemo(() => createTheme(getDesignTokens(settings['darkmode'] ? 'dark' : 'light' )), [settings]);

  return (
    <CacheProvider value={emotionCache}>
      <Head>
        <meta name="viewport" content="initial-scale=1, width=device-width" />
      </Head>
 
      <ThemeProvider theme={theme}>
        <ScopedCssBaseline enableColorScheme >
          {/* <Loader loading={loading} /> */}
          <Component {...pageProps} />
        </ScopedCssBaseline>
      </ThemeProvider>
    </CacheProvider>
  );
}

Advertisement

Answer

When you update the localStorage, the document that’s updating does not get noticed with its event listener. Here is a quote from mdn:

This won’t work on the same page that is making the changes — it is really a way for other pages on the domain using the storage to sync any changes that are made. Pages on other domains can’t access the same storage objects.

The solution is to add the below line after each localStorage.setItem(...):

window.dispatchEvent(new Event("storage"));

To adapt to your use case, create a function called for example updateTheme inside useLocalStorage:

const updateTheme = (value)=>{
  localStorage.setItem(key, JSON.stringify(value));
  window.dispatchEvent(new Event("storage"));
}

And you return the above function instead of setValue, like so:

return [value, updateTheme, page];    

And remove that useEffect you have:

useEffect(() => {
  localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement