如何在 React 应用程序中设置系统首选项黑暗模式,同时还允许用户来回切换当前主题

问题描述 投票:0回答:4

我有一个 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
方法正常工作?

这是有问题的网络应用程序

javascript css reactjs react-hooks react-context
4个回答
9
投票
对于所有想要

订阅系统范围配色方案更改的人:

我扩展了@Daniel Danielecki 的精彩答案:

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)); }, []);
通过向媒体查询添加事件监听器,您可以监听深色主题的变化。如果您的用户有基于当前时间的自适应暗/亮模式周期,这非常有用。


5
投票
尽管 Barry 的解决方案有效,但请注意,您无需添加更多代码,只需略读即可获得相同的结果:

关键是将用户的偏好设置为初始状态,并停止检查效果:

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 示例


3
投票
为什么不简单地使用

useEffect

useEffect(() => { const prefersDark = window.matchMedia( "(prefers-color-scheme: dark)" ).matches; if (prefersDark) { setIsDark(true); } }, []);

window

访问
useEffect
的原因:
Next.js React app中没有定义Window


2
投票
问题在于,每次

useLayoutEffect

 值发生变化时,整个 
dark
 块都会运行。因此,当用户切换 
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]);
您的切换组件不必更改。

更新:

萨尔的回答是一个很好的选择。我的指出了现有代码中的缺陷以及如何添加它。这指出了如何更有效地编写代码。

export function ThemeProvider({ children }) { const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const [dark, setDark] = useState(prefersDark); useLayoutEffect(() => { applyTheme(); }, [dark]); ... }
    
© www.soinside.com 2019 - 2024. All rights reserved.