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]);