我有一个 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
方法正常工作?
这是有问题的网络应用程序
订阅系统范围配色方案更改的人:
我扩展了@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));
}, []);
通过向媒体查询添加事件监听器,您可以监听深色主题的变化。如果您的用户有基于当前时间的自适应暗/亮模式周期,这非常有用。
关键是将用户的偏好设置为初始状态,并停止检查效果:
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]);
...
useEffect
?
useEffect(() => {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
if (prefersDark) {
setIsDark(true);
}
}, []);
从window
访问
useEffect
的原因:Next.js React app中没有定义Window。
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]);
...
}