You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题原因

  1. storage事件特性限制:storage事件仅在其他标签页修改localStorage时触发,当前标签页修改localStorage不会触发该事件,导致当前标签页的_app组件无法同步状态。
  2. 状态独立实例化:_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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 21:33:22