[英]How do I set system preference dark mode in a react app but also allow users to toggle back and forth the current theme
我有一個反應 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
function 的切換按鈕來切換主題。 在我當前的代碼中,當調用toggle
時,似乎 state 更改發生了兩次,因此主題保持不變。 如何確保toggle
方法正常工作?
這是有問題的web 應用程序
盡管 Barry 的解決方案有效,但請注意,您可以通過略讀獲得相同的結果,而不是添加更多代碼:
關鍵是將用戶的偏好設置為初始 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]);
...
問題是useLayoutEffect
的整個塊在每次dark
值更改時都會運行。 因此,當用戶切換dark
時,prefers prefers...
if 語句運行並將setDark
返回到系統首選項。
為了解決這個問題,您需要跟蹤用戶手動切換主題,然后阻止prefers...
if 語句運行。
在您的ThemeProvider
中執行以下操作:
const [userPicked, setUserPicked] = useState(false);
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);
};
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]);
...
}
為什么不簡單地使用useEffect
?
useEffect(() => {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
if (prefersDark) {
setIsDark(true);
}
}, []);
從useEffect
訪問window
的原因: Window 未在 Next.js React app 中定義。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.