通过 Context 在 React 中实现暗黑模式

2025-06-11

通过 Context 在 React 中实现暗黑模式

人们或许会认为我们生活的时间线已经够黑暗了,但不知何故,开发者们却执着于为用户提供让他们的生活更加黑暗的选择。在本文中,我们将介绍如何在 React 中实现暗模式和亮模式的切换。我们还将深入探讨用于实现此功能的工具——React Context。

让我们从一个简单的 React 应用开始,它包含 6 个组件:一个按钮、一个导航栏,以及一个嵌套的“家族”,由 Grandparent、Parent、Child 和 Baby 组成。最顶层的 App 组件包含导航栏和 GrandParent,而 GrandParent 又包含 Parent,Parent 又包含 Child,Child 又包含 Baby。



function App() {
  return (
    <>
      <Navbar />
      <div className="App-div">
        <GrandParent />
      </div>
    </>
  );
}


Enter fullscreen mode Exit fullscreen mode

为了方便变量命名,我们假设一个孩子生了一个婴儿,并且这个婴儿有祖父母而不是曾祖父母,这完全合乎逻辑。现在,我们先放下怀疑,看看下面在轻量模式下会是什么样子。

屏幕截图显示了灯光模式下的嵌套组件

附注:上面那些繁琐的代码只是为了让你清楚地明白,本文的重点并非 CSS,而是 React 逻辑的实现,它使我们能够轻松地在所有组件之间切换 CSS 类。如果你正在寻找美观的暗黑模式 CSS,请继续寻找,祝你好运。

最终的目标是让用户只需单击导航栏中的切换开关或图标即可在当前的亮模式和暗模式之间切换。

步骤 1:添加切换开关/图标

切换开关实际上是复选框类型的样式繁琐的输入框。没有人会从零开始实现切换开关。没有人。一个人也没有。除非他们喜欢 CSS,我听说只有少数人喜欢 CSS 😯 所以,我们来抓取一些代码,例如从这里获取,然后将我们的开关添加到导航栏。你也可以添加环绕太阳/月亮图标的按钮,例如从这里获取。我们的页面现在如下所示:

添加了切换开关的浅色主题

美丽的!

步骤2:组件之间共享数据

要实现暗黑模式,我们需要找到一种在组件之间高效共享数据的方法。假设在我们的示例中,GrandParent 组件想要与 Baby 组件共享一些内容。一种方法是在 GrandParent 级别定义一个变量或状态,并将其通过 Parent 和 Child 组件一直传递到 Baby 组件,如下所示:

GrandParent 定义变量并将其传递给 Parent。



const GrandParent = () => {
  const grandmasFavSong = "Toxic by B. Spears";
  return (
    <div className="GrandParent-div">
      <Parent grandmasFavSong={grandmasFavSong} />
      <div>I AM THE GRANDPARENT 👵  and my fav song is {grandmasFavSong}</div>
      <Button />
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

父进程解构了 grandmasFavSong 属性并将其传递给子进程。这很费劲……



const Parent = ({ grandmasFavSong }) => {
  return (
    <div className="Parent-div">
      <Child grandmasFavSong={grandmasFavSong} />
      <div>I AM THE PARENT 👩</div>
      <Button />
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

Child 现在还必须解构 prop 并将其传递给 Baby 组件。🥱🥱🥱



const Child = ({ grandmasFavSong }) => {
  return (
    <div className="Child-div">
      <Baby grandmasFavSong={grandmasFavSong} />
      <div>I AM THE CHILD 🧒 </div>
      <Button />
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

最后,婴儿部分知道了奶奶的秘密痴迷。



const Baby = ({ grandmasFavSong }) => {
  return (
    <div className="Baby-div">
      <div>
        I AM THE BABY 🍼  why is grandma making me listen to {grandmasFavSong}??
      </div>
      <Button />
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

支柱钻孔演示

你可能已经注意到,这并不是一种在组件之间实现数据共享的优雅方式。这种做法被称为prop 钻孔。它被认为是不好的做法,应该避免,就像它的表亲石油钻孔和牙齿钻孔一样。最好避免任何形式的钻孔。值得庆幸的是,React 提供了一个简洁的替代方案。

输入 React Context。

无论是在生活中还是在 React 中,上下文都是关键。React Context 提供了一种在组件之间共享数据的方法,而无需将其作为 props 逐层传递。相比于我们上面看到的方法,使用上述的 React Context 更能体现出祖母对 2000 年代流行音乐的痴迷。工作流程如下:

  1. 创建上下文
  2. 将要共享的数据添加到 Context
  3. 将 Context 提供程序包装在需要访问它的组件周围
  4. 在需要的地方使用 Context 提供程序

让我们一步一步地进行。

1. 创建上下文

我们将在名为 MusicContext.js 的新文件中执行此操作:



import React from "react";

export default React.createContext();


Enter fullscreen mode Exit fullscreen mode

就这些?是的,就这些。

2. 将要共享的数据添加到 Context

让我们创建一个名为 MusicProvider.js 的新文件。我们将在这里定义数据,并使用childrenprop 来确保 MusicProvider 所封装的每个组件都能访问这些值。



import React from "react";
import MusicContext from "./MusicContext";

const MusicProvider = ({ children }) => {
  const grandmasFavSong = "Toxic by B. Spears";
  return (
    <MusicContext.Provider value={grandmasFavSong}>
      {children}
    </MusicContext.Provider>
  );
};
export default MusicProvider;


Enter fullscreen mode Exit fullscreen mode
3. 将 Context 提供程序包装在相关组件周围

在本例中,我们不需要 Navbar 访问数据,但希望 GrandParent 和 Baby 组件能够访问。因此,我们将使用提供程序包装 GrandParent,所有其他 Family 组件都嵌套在其中。



import MusicProvider from "./Context/MusicProvider";

function App() {
  return (
    <>
      <Navbar />
      <div className="App-div">
        <MusicProvider>
          <GrandParent />
        </MusicProvider>
      </div>
    </>
  );
}


Enter fullscreen mode Exit fullscreen mode
4. 在需要的地方使用 Context

我们希望显示 GrandParent 和 Baby 组件中的数据。我们需要在每个文件中执行以下步骤:

  1. 从 React 导入 useContext hook
  2. 导入 MusicContext(不是MusicProvider)
  3. 从上下文中提取变量

让我们看看 Baby 组件是如何做到这一点的:



import React, { useContext } from "react";
import "./Family.css";
import Button from "./Button";
import MusicContext from "../Context/MusicContext";

const Baby = () => {
  // extracting variable from context ⬇️⬇️
  const grandmasFavSong = useContext(MusicContext);
  return (
    <div className="Baby-div">
      <div>
        I AM THE BABY 🍼  why is grandma making me listen to {grandmasFavSong}??
      </div>
      <Button />
    </div>
  );
};

export default Baby;


Enter fullscreen mode Exit fullscreen mode

对 GrandParent 执行相同的操作后,我们的应用应该看起来和以前一样了。虽然在我们这个小应用中,这比 prop 钻取更有效率地在组件间共享数据的方式目前还不明显,但相信我,使用 Context 的效用会随着应用规模和组件数量的增加而增长。

那么黑暗模式怎么样?

现在我们了解了 React Context,让我们用它来实现暗黑模式。实现暗黑模式的方法有很多,但这里我们将使用这个类dark ,并在 CSS 中将其与暗黑模式样式关联起来。该类dark将使用三元运算符有条件地在相关组件中渲染。我们以 Button 组件为例:



import React from "react";
import "./Button.css";

const Button = () => {
let darkMode = isDark ? "dark" : "";
  return (
    <button className={`Button-btn ${darkMode}`}>
      {isDark ? "Dark" : "Light "} button
    </button>
  );
};

export default Button;


Enter fullscreen mode Exit fullscreen mode

现在,让我们按照处理音乐环境时的相同步骤进行。

1. 在 ThemeContext.js 中创建上下文:


import React from "react";

export default React.createContext();


Enter fullscreen mode Exit fullscreen mode
2. 向 Context 提供程序添加值

我们将在名为 ThemeProvider.js 的文件中定义状态 isDark。我们还会定义一个用于切换 isDark 状态的函数。这两个值都将作为 Context 值传递给提供程序的子组件。由于这次我们有多个值,因此我们将把它们包装在一个对象中。



import React, { useState } from "react";
import ThemeContext from "./ThemeContext";

const ThemeProvider = ({ children }) => {
  const [isDark, setIsDark] = useState(false);
  const toggleMode = () => {
    setIsDark((mode) => !mode);
  };

  return (
    <ThemeContext.Provider value={{ isDark, toggleMode }}>
      {children}
    </ThemeContext.Provider>
  );
};

export default ThemeProvider;


Enter fullscreen mode Exit fullscreen mode
3. 将 Context 提供程序包装在相关组件周围

这次,我们希望将它包裹住所有组件,包括我们的导航栏。



import "./App.css";
import GrandParent from "./Family/GrandParent";
import "./Family/Family.css";
import Navbar from "./Navbar/Navbar";
import MusicProvider from "./Context/MusicProvider";
import ThemeProvider from "./Context/ThemeProvider";

function App() {
  return (
    <ThemeProvider>
      <Navbar />
      <div className="App-div">
        <MusicProvider>
          <GrandParent />
        </MusicProvider>
      </div>
    </ThemeProvider>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode
4. 在需要的地方使用 Context

让我们再次使用按钮组件作为例证:



import React, { useContext } from "react";
import "./Button.css";
import ThemeContext from "../Context/ThemeContext";

const Button = () => {
  const { isDark } = useContext(ThemeContext);
  let darkMode = isDark ? "dark" : "";
  return (
    <button className={`Button-btn ${darkMode}`}>
      {isDark ? "Dark" : "Light "} button
    </button>
  );
};

export default Button;


Enter fullscreen mode Exit fullscreen mode

在我们希望每个受模式切换影响的组件中都采用类似的方法之后,剩下唯一要做的就是实现它的切换功能。我们已经通过 Context 共享了切换功能,所以让我们在需要的地方使用它:在 ToggleSwitch 组件中。我们将创建一个在点击时触发并触发模式切换的事件。



import React, { useContext } from "react";
import "./ToggleSwitch.css";
import ThemeContext from "../Context/ThemeContext";

const ToggleSwitch = () => {
  const { toggleMode, isDark } = useContext(ThemeContext);

  return (
    <div className="ToggleSwitch-div">
      <label className="switch">
        <input onClick={toggleMode} type="checkbox" />
        <span class="slider round"></span>
      </label>
    </div>
  );
};

export default ToggleSwitch;


Enter fullscreen mode Exit fullscreen mode

欢呼!👏👏👏 大功告成!现在我们的应用看起来就像这样,或者说,效果确实好很多,这取决于我们在 CSS 上投入了多少精力。

在明暗模式之间切换的 GIF

鏂囩珷鏉ユ簮锛�https://dev.to/sanspanic/implementing-dark-mode-in-react-via-context-4f1p
PREV
如何通过编码面试(针对初级开发人员)
NEXT
使用 AWS(Amazon Web Services)实现无服务器 CI/CD 管道。