如何在反应应用程序中设置系统偏好暗模式,但还允许用户来回切换当前主题
Posted
技术标签:
【中文标题】如何在反应应用程序中设置系统偏好暗模式,但还允许用户来回切换当前主题【英文标题】:How do I set system preference dark mode in a react app but also allow users to toggle back and forth the current theme 【发布时间】:2020-07-21 20:43:29 【问题描述】:我有一个在导航上带有主题切换的 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
【问题讨论】:
我已经回答完了你的问题,这应该可以让你继续前进:) 【参考方案1】:虽然 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 example
【讨论】:
这样更干净也很聪明。【参考方案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]);
您的切换组件不必更改。
更新:
Sal 的回答是一个很好的选择。我指出了现有代码中的缺陷以及如何添加它。这指出了如何更有效地执行您的代码。
export function ThemeProvider( children )
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const [dark, setDark] = useState(prefersDark);
useLayoutEffect(() =>
applyTheme();
, [dark]);
...
【讨论】:
【参考方案3】:为什么不直接使用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。
【讨论】:
以上是关于如何在反应应用程序中设置系统偏好暗模式,但还允许用户来回切换当前主题的主要内容,如果未能解决你的问题,请参考以下文章
在 Xamarin Android 中设置布局背景 - 覆盖暗模式
如何以编程方式在系统偏好设置中设置 macOS 键盘快捷键?