localStorage更新不同步:暗黑模式切换在_app组件无法实时生效
问题:暗黑模式切换时当前标签页样式不更新,仅其他标签页生效
切换暗黑模式时,_app组件无法实时更新样式。打开两个标签页,在其中一个触发切换操作后,另一个标签页的暗黑模式会切换,但执行操作的标签页无变化。相关代码如下:
useSettings 自定义Hook代码
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> ); }
问题原因
- storage事件特性限制:
storage事件仅在其他标签页修改localStorage时触发,当前标签页修改localStorage不会触发该事件,导致当前标签页的_app组件无法同步状态。 - 状态独立实例化:_app组件和Index页面各自调用
useSettings,生成了两个独立的状态实例。Index页面修改的是自身的settings状态,_app组件的状态无法同步更新,仅能通过其他标签页的storage事件触发更新。
解决方案
方案1:使用React Context共享全局状态(推荐)
将settings状态提升到全局Context中,让所有组件共享同一状态,彻底避免localStorage同步问题。
步骤1:创建SettingsContext
// components/SettingsContext.tsx import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; type SettingsType = { darkmode: boolean; status: boolean; }; type SettingsContextType = { settings: SettingsType; setSettings: (newSettings: SettingsType) => void; }; const SettingsContext = createContext<SettingsContextType | undefined>(undefined); export function SettingsProvider({ children }: { children: ReactNode }) { const [settings, setSettings] = useState<SettingsType>({ darkmode: false, status: true, }); // 初始化从localStorage读取状态 useEffect(() => { const savedSettings = localStorage.getItem('settings'); if (savedSettings) { setSettings(JSON.parse(savedSettings)); } }, []); // 状态变化时同步到localStorage,并监听其他标签页的storage变化 useEffect(() => { localStorage.setItem('settings', JSON.stringify(settings)); const handleStorageChange = (e: StorageEvent) => { if (e.key === 'settings') { setSettings(JSON.parse(e.newValue || JSON.stringify({ darkmode: false, status: true }))); } }; window.addEventListener('storage', handleStorageChange); return () => window.removeEventListener('storage', handleStorageChange); }, [settings]); return ( <SettingsContext.Provider value={{ settings, setSettings }}> {children} </SettingsContext.Provider> ); } export function useSettings() { const context = useContext(SettingsContext); if (!context) { throw new Error('useSettings must be used within a SettingsProvider'); } return context; }
步骤2:在_app组件中包裹SettingsProvider
// pages/_app.tsx import { SettingsProvider } from '../components/SettingsContext'; export default function MyApp(props: MyAppProps) { const { Component, emotionCache = clientSideEmotionCache, pageProps } = props; const { settings } = useSettings(); 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> <SettingsProvider> <ThemeProvider theme={theme}> <ScopedCssBaseline enableColorScheme > <Component {...pageProps} /> </ScopedCssBaseline> </ThemeProvider> </SettingsProvider> </CacheProvider> ); }
步骤3:在Index页面中使用useSettings
// pages/index.tsx import type { NextPage } from 'next' import IOSToggle from '../components/IOSToggle' import { useSettings } from '../components/SettingsContext' const Home: NextPage = () => { const { settings, setSettings } = useSettings(); const HandleIosToggle = () => { setSettings({ ...settings, darkmode: !settings.darkmode }) } return ( <IOSToggle label={"Darkmode"} checked={settings.darkmode} onChange={HandleIosToggle} /> ); } export default Home
方案2:修复useLocalStorage的同步逻辑
如果不想使用Context,可以修改useLocalStorage,通过自定义事件让当前页的所有实例同步状态:
const useLocalStorage = (key: any, page: string, defaultValue: object) => { const [value, setValue] = useState<any | null>(defaultValue); const syncState = () => { const stickyValue = localStorage.getItem(key); if (stickyValue) { setValue(JSON.parse(stickyValue)); } }; const onStorageUpdate = (e: StorageEvent) => { if (e.key === key) { syncState(); } }; // 初始化读取+监听自定义同步事件 useLayoutEffect(() => { syncState(); const handleCustomUpdate = () => syncState(); window.addEventListener('localStorageUpdate', handleCustomUpdate); return () => window.removeEventListener('localStorageUpdate', handleCustomUpdate); }, [key]); const customSetValue = (newValue: any) => { setValue(newValue); localStorage.setItem(key, JSON.stringify(newValue)); // 触发自定义事件,通知当前页其他实例更新 window.dispatchEvent(new Event('localStorageUpdate')); }; useEffect(() => { window.addEventListener('storage', onStorageUpdate); return () => window.removeEventListener('storage', onStorageUpdate); }, [key]); return [value, customSetValue, page]; }
修改后调用customSetValue时,会触发自定义事件,当前页的所有useLocalStorage实例都会同步更新状态。
内容的提问来源于stack exchange,提问作者Fnug
相关产品推荐
相关产品推荐

