css 如何在react应用程序中设置系统偏好暗模式,同时允许用户来回切换当前主题

bq9c1y66  于 2023-02-20  发布在  React
关注(0)|答案(4)|浏览(144)

我有一个react web应用程序,在导航栏上有主题切换功能。我有一个ThemeProvider Context,它有逻辑自动检测用户的系统主题偏好并设置它。然而,我觉得用户应该能够在网站上来回切换主题,而不管他们的系统偏好。下面是ThemeContext.js文件,包含所有的主题逻辑,包括toggle方法。

import React, { useState, useLayoutEffect } from 'react';

const ThemeContext = React.createContext({
    dark: false,
    toggle: () => {},
});

export default ThemeContext;

export function ThemeProvider({ children }) {
    // keeps state of the current theme
    const [dark, setDark] = useState(false);

    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
        .matches;
    const prefersLight = window.matchMedia('(prefers-color-scheme: light)')
        .matches;
    const prefersNotSet = window.matchMedia(
        '(prefers-color-scheme: no-preference)'
    ).matches;

    // paints the app before it renders elements
    useLayoutEffect(() => {
        // Media Hook to check what theme user prefers
        if (prefersDark) {
            setDark(true);
        }

        if (prefersLight) {
            setDark(false);
        }

        if (prefersNotSet) {
            setDark(true);
        }

        applyTheme();

        // if state changes, repaints the app
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [dark]);

    // rewrites set of css variablels/colors
    const applyTheme = () => {
        let theme;
        if (dark) {
            theme = darkTheme;
        }
        if (!dark) {
            theme = lightTheme;
        }

        const root = document.getElementsByTagName('html')[0];
        root.style.cssText = theme.join(';');
    };

    const toggle = () => {
        console.log('Toggle Method Called');

        // A smooth transition on theme switch
        const body = document.getElementsByTagName('body')[0];
        body.style.cssText = 'transition: background .5s ease';

        setDark(!dark);
    };

    return (
        <ThemeContext.Provider
            value={{
                dark,
                toggle,
            }}>
            {children}
        </ThemeContext.Provider>
    );
}

// styles
const lightTheme = [
    '--bg-color: var(--color-white)',
    '--text-color-primary: var(--color-black)',
    '--text-color-secondary: var(--color-prussianBlue)',
    '--text-color-tertiary:var(--color-azureRadiance)',
    '--fill-switch: var(--color-prussianBlue)',
    '--fill-primary:var(--color-prussianBlue)',
];

const darkTheme = [
    '--bg-color: var(--color-mirage)',
    '--text-color-primary: var(--color-white)',
    '--text-color-secondary: var(--color-iron)',
    '--text-color-tertiary: var(--color-white)',
    '--fill-switch: var(--color-gold)',
    '--fill-primary:var(--color-white)',
];

因此,当页面加载时,显示用户的系统首选它们,但也允许用户通过单击触发toggle函数的切换按钮来切换主题。在我当前的代码中,当调用toggle时,似乎状态更改发生了两次,因此主题保持不变。我如何确保toggle方法正确工作?
下面是所讨论的web app

zf2sa74q

zf2sa74q1#

尽管巴里的解决方案是可行的,但请注意,您不必添加更多代码,只需略读代码即可获得相同的结果:
关键是将用户的偏好设置为初始状态,并在效果中停止勾选:

export function ThemeProvider({ children }) {
    /* Because you are setting the initial theme to non-dark, 
    you can assume that your initial state should be dark only 
    when the user's preference is set to dark. */
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
        .matches;

    // True if preference is set to dark, false otherwise.
    const [dark, setDark] = useState(prefersDark);
    /* Note: Initial state is set upon mounting, hence is better 
    to put the <ThemeProvider> up in your tree, close to the root <App> 
    to avoid unmounting it with the result of reverting to the default user 
    preference when and if re-mounting (unless you want that behaviour) */

    useLayoutEffect(() => {
        /* You end up here only when the user takes action 
        to change the theme, hence you can just apply the new theme. */
        applyTheme();
}, [dark]);
...

CodeSandbox example

7d7tgy0s

7d7tgy0s2#

为什么不简单地使用useEffect呢?

useEffect(() => {
  const prefersDark = window.matchMedia(
    "(prefers-color-scheme: dark)"
  ).matches;

  if (prefersDark) {
    setIsDark(true);
  }
}, []);

useEffect访问window的原因:Window is not defined in Next.js React app .

sirbozc5

sirbozc53#

问题是dark的值每次改变,useLayoutEffect的整个块都会运行,所以当用户切换dark时,prefers...if语句运行,setDark返回到系统首选项。
要解决这个问题,您需要跟踪用户手动切换主题,然后阻止prefers... if语句运行。
ThemeProvider中执行以下操作:

  • 如果用户已使用切换,则添加要监视的状态
const [userPicked, setUserPicked] = useState(false);
  • 更新您的toggle函数:
const toggle = () => {
  console.log('Toggle Method Called');

  const body = document.getElementsByTagName('body')[0];
  body.style.cssText = 'transition: background .5s ease';

  setUserPick(true) // Add this line
  setDark(!dark);
};
  • 最后,将useLayout更新为如下所示:
useLayoutEffect(() => {
  if (!userPicked) { // This will stop the system preferences from taking place if the user manually toggles the them
    if (prefersDark) {
      setDark(true);
    }

    if (prefersLight) {
      setDark(false);
    }

    if (prefersNotSet) {
      setDark(true);
    }
  }

  applyTheme();
}, [dark]);

您的切换组件不需要更改。

    • 更新日期:**

Sal的答案是一个很好的选择。我的答案指出了现有代码中的缺陷以及如何添加。这指出了如何更有效地添加代码。

export function ThemeProvider({ children }) {
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

  const [dark, setDark] = useState(prefersDark);

  useLayoutEffect(() => {
    applyTheme();
  }, [dark]);

  ...

}
k4ymrczo

k4ymrczo4#

对于希望订阅系统范围配色方案更改的每个人:
我扩展了“丹尼尔·丹尼莱茨基的伟大回答:

useEffect(() => {
  const mq = window.matchMedia(
    "(prefers-color-scheme: dark)"
  );

  if (mq.matches) {
    setIsDark(true);
  }

  // This callback will fire if the perferred color scheme changes without a reload
  mq.addEventListener("change", (evt) => setIsDark(evt.matches === "dark"));
}, []);

通过在媒体查询中添加一个事件监听器,你可以监听黑暗主题的变化。如果你的用户有一个基于当前时间的自适应黑暗/明亮模式循环,这是很有用的。

相关问题