简体   繁体   English

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

[英]How do I set system preference dark mode in a react app but also allow users to toggle back and forth the current theme

I have a react web app with a theme toggle on the navigation.我有一个反应 web 应用程序,导航上有一个主题切换。 I have a ThemeProvider Context that has logic to auto detects a user's System theme preference and sets it.我有一个ThemeProvider Context ,它具有自动检测用户的系统主题偏好并设置它的逻辑。 However, I feel a user should be able to toggle themes back and forth on the website despite their system preference.但是,我觉得用户应该能够在网站上来回切换主题,尽管他们有系统偏好。 Here is the ThemeContext.js file with all the theme logic including the toggle method.这是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)',
];

So when the page loads, show the user's system preferred them but also allow user to toggle themes by clicking a toggle button that fires the toggle function.因此,当页面加载时,显示用户的系统首选它们,但也允许用户通过单击触发toggle function 的切换按钮来切换主题。 In my current code, when toggle is called, it seems that state changes occur twice and therefore theme remains unchanged.在我当前的代码中,当调用toggle时,似乎 state 更改发生了两次,因此主题保持不变。 How do I ensure the toggle method works correctly?如何确保toggle方法正常工作?

Here is the web app in question这是有问题的web 应用程序

Although Barry's solution is working, note that instead of adding more code, you could achieve the same result by skimming it:尽管 Barry 的解决方案有效,但请注意,您可以通过略读获得相同的结果,而不是添加更多代码:

The key is to set the user's preference as initial state and stop checking it in the effect:关键是将用户的偏好设置为初始 state 并在效果中停止检查:

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 代码沙盒示例

The problem is that the whole block of useLayoutEffect runs every the dark value changes.问题是useLayoutEffect的整个块在每次dark值更改时都会运行。 So when the user toggles dark , the prefers... if statements run and setDark back to the system preference.因此,当用户切换dark时,prefers prefers... if 语句运行并将setDark返回到系统首选项。

To solve this you'll need to keep track of the user manually toggling the theme and then preventing the prefers... if statements from running.为了解决这个问题,您需要跟踪用户手动切换主题,然后阻止prefers... if 语句运行。

In your ThemeProvider do the following:在您的ThemeProvider中执行以下操作:

  • Add a state to monitor if the user has used toggle添加 state 以监控用户是否使用了切换
const [userPicked, setUserPicked] = useState(false);
  • Update your toggle function:更新您的toggle function:
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);
};
  • Finally, update the useLayout to look like this:最后,将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]);

Your toggle component shouldn't have to change.您的切换组件不必更改。

Update:更新:

Sal's answer is a great alternative.萨尔的回答是一个很好的选择。 Mine points out the flaw in existing code and how to add to it.我指出了现有代码中的缺陷以及如何添加它。 This points out how to which your code more effectively.这指出了如何更有效地执行您的代码。

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

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

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

  ...

}

Why don't use simply useEffect ?为什么不简单地使用useEffect

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

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

The reason to access window from useEffect : Window is not defined in Next.js React app .useEffect访问window的原因: Window 未在 Next.js React app 中定义

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何来回切换插入符号? - How do I toggle the caret back and forth? 如何跨多个页面设置暗模式主题? - How do i set dark mode theme across multiple pages? 如何强制更新我的反应代码中的组件以将深色主题切换为浅色主题 - How do I force Update my components in my react code to toggle dark theme to light theme 如何使用 Javascript 正确构建暗模式切换? - How do I properly structure a dark mode toggle using Javascript? 当我在 vuejs 中单击它时,如何将 div 中的文本从“暗”和“亮”来回切换? - How do I switch text in a div from 'dark' and 'light' back and forth when i click it in vuejs? 我如何获得一个切换按钮来将数组从降序切换到升序? - How do I get a toggle button to toggle an array back and forth from descending to ascending? 通过更改暗模式切换,我还想更改引导程序 class - By changing the dark mode toggle, I also want to change the bootstrap class 当当前主题存储在本地时,如何在浅色和深色主题之间切换? - How do you toggle between light and dark themes while current theme is being locally stored? 如何使用 localStorage 使浏览器记住亮/暗模式主题切换的类列表切换状态? - How can I use localStorage to make the browser remember a classlist toggle state for a light/dark mode theme switch? (暗模式)添加“暗主题”后,如何更改背景? - (Dark Mode) How do I get the background to change after the “dark-theme” is added?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM