admin管理员组文章数量:1201410
I have a react web app with a theme toggle on the navigation. I have a ThemeProvider Context
that has logic to auto detects a user's System theme preference and sets it. 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.
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. In my current code, when toggle
is called, it seems that state changes occur twice and therefore theme remains unchanged. How do I ensure the toggle
method works correctly?
Here is the web app in question
I have a react web app with a theme toggle on the navigation. I have a ThemeProvider Context
that has logic to auto detects a user's System theme preference and sets it. 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.
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. In my current code, when toggle
is called, it seems that state changes occur twice and therefore theme remains unchanged. How do I ensure the toggle
method works correctly?
Here is the web app in question
Share Improve this question asked Apr 9, 2020 at 9:07 CozyCozy 3631 gold badge6 silver badges18 bronze badges 1- 1 I've finished answering your question, this should get you going :) – Barry Michael Doyle Commented Apr 9, 2020 at 9:30
4 Answers
Reset to default 10For everyone who wants to subscribe to changes of the system wide color scheme:
I extended @Daniel Danielecki's great answer:
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));
}, []);
By adding an event listener to the media query, you can listen to changes in the dark theme. This is useful if your user has an adaptive dark/light mode cycle based on their current time.
Although Barry's solution is working, note that instead of adding more code, you could achieve the same result by skimming it:
The key is to set the user's preference as initial state and stop checking it in the effect:
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
Why don't use simply 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.
The problem is that the whole block of useLayoutEffect
runs every the dark
value changes. So when the user toggles dark
, the prefers...
if statements run and setDark
back to the system preference.
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.
In your ThemeProvider
do the following:
- Add a state to monitor if the user has used toggle
const [userPicked, setUserPicked] = useState(false);
- Update your
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:
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 write 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]);
...
}
本文标签:
版权声明:本文标题:javascript - How do I set system preference dark mode in a react app but also allow users to toggle back and forth the current t 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1738569577a2100513.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论