亮/暗模式:React 实现
介绍
在之前的文章中,我们了解了如何:
- 使用 CSS 处理不同的主题,
- 处理系统主题和用户选择的主题,
- 存储之前选择的主题以供下次访问,
- 如何避免页面重新加载时主题闪烁。
在本文中,我们将了解如何将所有组件整合在一起,并在其中添加React
和远程数据库(出于趣味)。 目标是展示实际应用中处理主题所需的代码主干。
目录
我们将要实现的逻辑流程
以下流程与前端应用程序相关,而不是服务器端呈现的网站(就像您在 PHP 中看到的那样):
- 用户正在加载您的网站
- 我们正在(以阻止的方式)应用先前选择的主题(可能是错误的)
- 对数据库进行提取以检索他们最喜欢的模式(亮/暗/系统)
- 最喜欢的模式会保存在浏览器中以供将来访问
- 该模式保存在反应上下文中(如果需要,可以进行反应更新)
- 当模式改变时,它会被保存在本地(以供将来使用),对数据库执行请求,并更新反应上下文。
首次造访
您的用户将不会在数据库中有任何条目,也不会保存任何本地数据。因此,我们将使用系统模式作为后备。
首次使用新浏览器访问
您的用户将没有任何本地数据,因此当针对您的数据库执行请求以检索他们的首选模式时,我们将使用系统模式来避免不必要的闪烁。
再次访问
用户之前在此浏览器上选择的模式将被优先选择。然后有两种可能性:
- 他们没有在其他设备上更改其首选模式,因此本地模式与远程模式匹配 => 没有差异,也没有闪烁(这是页面刷新期间的流程),
- 他们已经改变了它,在这里我们将在第一次重新访问时看到一个小闪光(但我们无法阻止它)
结果
解释
HTML
CSS
我对 CSS 做了一些简单的处理:一个data-theme
具有 2 个值light
和的数据属性dark
,并且我更新了 2 个 css 变量,最终控制主体的外观。
和本系列的所有其他文章一样,我们需要设置color-scheme
,以确保原生元素能够响应正确的主题:
:root[data-theme="light"] {
color-scheme: light;
--color: #111;
--background: #fff;
}
:root[data-theme="dark"] {
color-scheme: dark;
--color: #cecece;
--background: #333;
}
body {
color: var(--color);
background: var(--background);
}
阻止脚本
由于我们希望避免页面加载期间出现闪烁,我添加了一个小的阻止脚本标签,该标签仅执行同步操作,仅检查最基本的要求以确定要显示的最佳主题:
<script>
const mode = localStorage.getItem("mode") || "system";
let theme;
if (mode === "system") {
const isSystemInDarkMode = matchMedia("(prefers-color-scheme: dark)")
.matches;
theme = isSystemInDarkMode ? "dark" : "light";
} else {
// for light and dark, the theme is the mode
theme = mode;
}
document.documentElement.dataset.theme = theme;
</script>
JavaScript
基础变量
首先,我们需要确定我们的变量:我将用于mode
保存的模式(亮/暗/系统)和theme
视觉主题(亮/暗):
// Saved mode
type Mode = "light" | "dark" | "system";
// Visual themes
type Theme = "light" | "dark";
反应上下文
由于我们希望能够提供有关当前模式/主题的一些信息以及为用户提供更改模式的方法,因此我们将创建一个包含所有内容的 React 上下文:
const ThemeContext = React.createContext<{
mode: Mode;
theme: Theme;
setMode: (mode: Mode) => void;
}>({
mode: "system",
theme: "light",
setMode: () => {}
});
模式初始化
我们将使用状态(因为它的值可以更改,并且应该触发更新)来存储模式。
使用,你可以提供一个称为惰性初始状态React.useState
的函数,该函数仅在第一次渲染期间调用:
const [mode, setMode] = React.useState<Mode>(() => {
const initialMode =
(localStorage.getItem(localStorageKey) as Mode | undefined) || "system";
return initialMode;
});
数据库同步
现在我们有了mode
状态,我们需要使用远程数据库来更新它。为此,我们可以使用 effect,但我决定使用另一个useState
,这看起来很奇怪,因为我没有使用返回的状态,但如上所述,惰性初始状态仅在第一次渲染期间调用。
这允许我们在渲染期间启动后端调用,而不是在 effect 之后。而且,由于我们更早地启动了 API 调用,我们也能更快地收到响应:
// This will only get called during the 1st render
React.useState(() => {
getMode().then(setMode);
});
保存回模式
当模式改变时,我们希望:
- 将其保存在本地存储中(以避免重新加载时闪烁)
- 在数据库中(用于跨设备支持)
效果是完美的用例:我们传递mode
依赖项数组,以便每次模式改变时都会调用该效果:
React.useEffect(() => {
localStorage.setItem(localStorageKey, mode);
saveMode(mode); // database
}, [mode]);
模式初始化
现在我们已经有了获取、保存和更新模式的方法,接下来我们需要将其转换为视觉主题。
为此,我们将使用另一个状态(因为主题更改应该触发更新)。
我们将使用另一个惰性初始状态来将system
模式与用户为其设备选择的主题同步:
const [theme, setTheme] = React.useState<Theme>(() => {
if (mode !== "system") {
return mode;
}
const isSystemInDarkMode = matchMedia("(prefers-color-scheme: dark)")
.matches;
return isSystemInDarkMode ? "dark" : "light";
});
系统主题更新
如果用户选择了该system
模式,我们需要追踪他们是否决定在系统模式下将其从亮变为暗(这就是我们也使用状态的原因theme
)。
为此,我们还将使用一个可以检测模式变化的效果。此外,当用户处于该system
模式时,我们将获取他们当前的系统主题,并启动一个事件监听器来检测其主题的任何变化:
React.useEffect(() => {
if (mode !== "system") {
setTheme(mode);
return;
}
const isSystemInDarkMode = matchMedia("(prefers-color-scheme: dark)");
// If system mode, immediately change theme according to the current system value
setTheme(isSystemInDarkMode.matches ? "dark" : "light");
// As the system value can change, we define an event listener when in system mode
// to track down its changes
const listener = (event: MediaQueryListEvent) => {
setTheme(event.matches ? "dark" : "light");
};
isSystemInDarkMode.addListener(listener);
return () => {
isSystemInDarkMode.removeListener(listener);
};
}, [mode]);
将主题应用回 HTML
现在我们有了可靠的theme
状态,我们可以让 CSS 和 HTML 遵循这个状态:
React.useEffect(() => {
// Clear previous theme on the html and set the new one
document.documentElement.dataset.theme = theme;
}, [theme]);
定义上下文
现在我们已经有了所需的所有变量,最后要做的就是将整个应用程序包装在上下文提供程序中:
<ThemeContext.Provider value={{ theme, mode, setMode }}>
{children}
</ThemeContext.Provider>
当我们需要引用它时,我们可以这样做:
const { theme, mode, setMode } = React.useContext(ThemeContext);
结论
处理多个主题并不是一件简单的事情,特别是当您想为用户提供最佳体验,同时为您的同事开发人员提供方便的工具时。
这里我仅介绍了一种可能的处理方法,它可以针对其他用例进行改进、完善和扩展。
但即使您的逻辑/要求不同,一开始提出的流程也不应该与您应该采用的流程有太大不同。
如果您想查看我在示例中编写的完整代码,可以在这里找到:https://codesandbox.io/s/themes-tbclf。
文章来源:https://dev.to/ayc0/light-dark-mode-react-implementation-3aoa