亮/暗模式:React 实现

2025-05-27

亮/暗模式:React 实现

介绍

在之前的文章中,我们了解了如何:

  • 使用 CSS 处理不同的主题,
  • 处理系统主题和用户选择的主题,
  • 存储之前选择的主题以供下次访问,
  • 如何避免页面重新加载时主题闪烁。

在本文中,我们将了解如何将所有组件整合在一起,并在其中添加React
和远程数据库(出于趣味)。 目标是展示实际应用中处理主题所需的代码主干。

目录

  1. 我们将要实现的逻辑流程
    1. 首次造访
    2. 首次使用新浏览器访问
    3. 再次访问
  2. 结果
  3. 解释
    1. HTML
      1. CSS
      2. 阻止脚本
    2. JavaScript
      1. 基础变量
      2. 反应上下文
      3. 模式初始化
      4. 数据库同步
      5. 保存回模式
      6. 模式初始化
      7. 系统主题更新
      8. 将主题应用回 HTML
      9. 定义上下文
  4. 结论

我们将要实现的逻辑流程

以下流程与前端应用程序相关,而不是服务器端呈现的网站(就像您在 PHP 中看到的那样):

  1. 用户正在加载您的网站
  2. 我们正在(以阻止的方式)应用先前选择的主题(可能是错误的)
  3. 对数据库进行提取以检索他们最喜欢的模式(亮/暗/系统)
  4. 最喜欢的模式会保存在浏览器中以供将来访问
  5. 该模式保存在反应上下文中(如果需要,可以进行反应更新)
  6. 当模式改变时,它会被保存在本地(以供将来使用),对数据库执行请求,并更新反应上下文。

首次造访

您的用户将不会在数据库中有任何条目,也不会保存任何本地数据。因此,我们将使用系统模式作为后备。

首次使用新浏览器访问

您的用户将没有任何本地数据,因此当针对您的数据库执行请求以检索他们的首选模式时,我们将使用系统模式来避免不必要的闪烁。

再次访问

用户之前在此浏览器上选择的模式将被优先选择。然后有两种可能性:

  • 他们没有在其他设备上更改其首选模式,因此本地模式与远程模式匹配 => 没有差异,也没有闪烁(这是页面刷新期间的流程),
  • 他们已经改变了它,在这里我们将在第一次重新访问时看到一个小闪光(但我们无法阻止它)

结果

解释

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);
}
Enter fullscreen mode Exit fullscreen mode

阻止脚本

由于我们希望避免页面加载期间出现闪烁,我添加了一个小的阻止脚本标签,该标签仅执行同步操作,仅检查最基本要求以确定要显示的最佳主题:

<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>
Enter fullscreen mode Exit fullscreen mode

JavaScript

基础变量

首先,我们需要确定我们的变量:我将用于mode保存的模式(亮/暗/系统)和theme视觉主题(亮/暗):

// Saved mode
type Mode = "light" | "dark" | "system";
// Visual themes
type Theme = "light" | "dark";
Enter fullscreen mode Exit fullscreen mode

反应上下文

由于我们希望能够提供有关当前模式/主题的一些信息以及为用户提供更改模式的方法,因此我们将创建一个包含所有内容的 React 上下文:

const ThemeContext = React.createContext<{
  mode: Mode;
  theme: Theme;
  setMode: (mode: Mode) => void;
}>({
  mode: "system",
  theme: "light",
  setMode: () => {}
});
Enter fullscreen mode Exit fullscreen mode

模式初始化

我们将使用状态(因为它的值可以更改,并且应该触发更新)来存储模式。
使用,你可以提供一个称为惰性初始状态React.useState的函数,该函数仅在第一次渲染期间调用:

const [mode, setMode] = React.useState<Mode>(() => {
  const initialMode =
    (localStorage.getItem(localStorageKey) as Mode | undefined) || "system";
  return initialMode;
});
Enter fullscreen mode Exit fullscreen mode

数据库同步

现在我们有了mode状态,我们需要使用远程数据库来更新它。为此,我们可以使用 effect,但我决定使用另一个useState,这看起来很奇怪,因为我没有使用返回的状态,但如上所述,惰性初始状态仅在第一次渲染期间调用。
这允许我们在渲染期间启动后端调用,而不是在 effect 之后。而且,由于我们更早地启动了 API 调用,我们也能更快地收到响应:

// This will only get called during the 1st render
React.useState(() => {
  getMode().then(setMode);
});
Enter fullscreen mode Exit fullscreen mode

保存回模式

当模式改变时,我们希望:

  • 将其保存在本地存储中(以避免重新加载时闪烁)
  • 在数据库中(用于跨设备支持)

效果是完美的用例:我们传递mode依赖项数组,以便每次模式改变时都会调用该效果:

React.useEffect(() => {
  localStorage.setItem(localStorageKey, mode);
  saveMode(mode); // database
}, [mode]);
Enter fullscreen mode Exit fullscreen 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";
});
Enter fullscreen mode Exit fullscreen mode

系统主题更新

如果用户选择了该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]);
Enter fullscreen mode Exit fullscreen mode

将主题应用回 HTML

现在我们有了可靠的theme状态,我们可以让 CSS 和 HTML 遵循这个状态:

React.useEffect(() => {
  // Clear previous theme on the html and set the new one
  document.documentElement.dataset.theme = theme;
}, [theme]);
Enter fullscreen mode Exit fullscreen mode

定义上下文

现在我们已经有了所需的所有变量,最后要做的就是将整个应用程序包装在上下文提供程序中:

<ThemeContext.Provider value={{ theme, mode, setMode }}>
  {children}
</ThemeContext.Provider>
Enter fullscreen mode Exit fullscreen mode

当我们需要引用它时,我们可以这样做:

const { theme, mode, setMode } = React.useContext(ThemeContext);
Enter fullscreen mode Exit fullscreen mode

结论

处理多个主题并不是一件简单的事情,特别是当您想为用户提供最佳体验,同时为您的同事开发人员提供方便的工具时。

这里我仅介绍了一种可能的处理方法,它可以针对其他用例进行改进、完善和扩展。

但即使您的逻辑/要求不同,一开始提出的流程也不应该与您应该采用的流程有太大不同。

如果您想查看我在示例中编写的完整代码,可以在这里找到:https://codesandbox.io/s/themes-tbclf

文章来源:https://dev.to/ayc0/light-dark-mode-react-implementation-3aoa
PREV
尝试 gh,GitHub 的新 CLI
NEXT
为 AWS 架构图增添趣味:创建 AWS 架构动画 GIF 的分步指南