当前位置: 首页 > news >正文

第二章、全局配置项目主题色(主题切换+跟随系统)

全局配置项目主题色(主题切换)

React + TypeScript 项目中实现 全局主题切换(暗黑/亮色模式) 通常有三种常见方案:

1️⃣ CSS变量方案(推荐,简单易维护) 适合需要动态主题切换和性能要求高的项目;
2️⃣ CSS-in-JS(如 styled-components、Emotion)方案
3️⃣ UI库自带方案(如 Ant Design、MUI 的 ThemeProvider)

一、总体思路

全局主题切换的核心是:

不重新渲染页面,通过切换 data-themeclass 来控制 CSS 变量,从而动态改变颜色。

目录结构

src/├─ context/│   └─ ThemeContext.tsx├─ styles/│   └─ globals.css├─ index.css├─ App.tsx├─ index.tsx
themes/├─ dark.css├─ light.css

二、实现步骤

1️⃣ 定义全局主题变量

创建 themes/dark.css

html[data-theme="dark"] {--primary: #5dade2;--secondary: #58d68d;--accent: #e74c3c;--background: #121212;--surface: #1e1e1e;--text: #e0e0e0;--text-secondary: 

创建 themes/light.css

html[data-theme="light"] {--primary: #2980b9;--secondary: #1abc9c;--accent: #f39c12;--background: #ecf0f1;--surface: #ffffff;--text: #2c3e50;--text-secondary:

创建 src/styles/globals.css

@import '../themes/dark.css';
@import '../themes/light.css';body {font-family: sans-serif;background-color: var(--background);padding: 20px;color: var(--text);
}
button {background-color: var(--primary);color: var(--surface);
}
:root {--primary: #3498db;--secondary: #2ecc71;--background: #f8f9fa;--surface: #ffffff;--text: #333333;--border: #e0e0e0;
}

2️⃣ 在全局样式中使用变量

src/index.css中:

body {font-family: sans-serif;background-color: var(--background);padding: 20px;color: var(--text);
}
button {background-color: var(--primary);color: var(--surface);
}

3️⃣ 在 React 中控制主题

实现思路

  1. 定义一个 ThemeContext,保存当前主题(light/dark)和修改函数。
  2. ThemeProvider 组件包裹整个应用。
  3. 在任何组件中通过 useContext 获取当前主题和切换方法。
  4. 通过设置 <html data-theme=""> 属性或 CSS 变量来动态切换主题。

src/context/ThemeContext.tsx中, 创建 ThemeContext

import React, { createContext, useContext, useState, useEffect } from "react";type Theme = "light" | "dark";interface ThemeContextType {theme: Theme;toggleTheme: () => void;setTheme: (theme: Theme) => void;
}const ThemeContext = createContext<ThemeContextType | undefined>(undefined);export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {const [theme, setThemeState] = useState<Theme>((localStorage.getItem("theme") as Theme) || "light");// 切换主题const toggleTheme = () => {const newTheme = theme === "light" ? "dark" : "light";setThemeState(newTheme);document.documentElement.setAttribute("data-theme", newTheme);localStorage.setItem("theme", newTheme);};// 设置主题const setTheme = (newTheme: Theme) => {setThemeState(newTheme);document.documentElement.setAttribute("data-theme", newTheme);localStorage.setItem("theme", newTheme);};// 初始加载时同步useEffect(() => {document.documentElement.setAttribute("data-theme", theme);}, [theme]);return (<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>{children}</ThemeContext.Provider>);
};// 封装一个 Hook 方便使用
export const useTheme = (): ThemeContextType => {const context = useContext(ThemeContext);if (!context) throw new Error("useTheme must be used within a ThemeProvider");return context;
};

4️⃣在入口文件包裹 Provider

src/index.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { ThemeProvider } from "./context/ThemeContext";
import "./styles/globals.css";
import "./index.css";ReactDOM.createRoot(document.getElementById("root")!).render(<ThemeProvider><App /></ThemeProvider>
);

5️⃣在组件中使用主题切换

import React from "react";
import { useTheme } from "./context/ThemeContext";const App: React.FC = () => {const { theme, toggleTheme } = useTheme();return (<div style={{ padding: 20 }}><h1>🌗 当前主题:{theme}</h1><button onClick={toggleTheme}>切换为 {theme === "light" ? "暗黑模式" : "亮色模式"}</button><p>这是示例文本,会随主题颜色变化。</p></div>);
};export default App;

说明:

  • 初次加载时会根据 localStorage 读取上次主题。

  • 点击按钮后会:

    1. 修改 React state
    2. 更新 <html data-theme="dark">
    3. 自动切换 CSS 变量样式

整个应用的主题瞬间切换,无需刷新 。

三、支持系统偏好(跟随系统主题)

系统主题检测说明

const media = window.matchMedia("(prefers-color-scheme: dark)");
  • window.matchMedia() 是浏览器API,用于检测媒体查询
  • (prefers-color-scheme: dark) 查询系统是否启用深色模式
  • media.matches 返回布尔值:true表示系统是深色模式,false表示浅色模式

在这里插入图片描述

src/context/ThemeContext.tsx

import React, {createContext,useContext,useEffect,useState,useCallback,
} from "react";type Theme = "light" | "dark";interface ThemeContextType {theme: Theme;toggleTheme: () => void;setTheme: (theme: Theme) => void;
}const ThemeContext = createContext<ThemeContextType | undefined>(undefined);export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({children,
}) => {const getInitialTheme = (): Theme => {const local = localStorage.getItem("theme") as Theme | null;if (local) return local;// 没有保存的主题时,根据系统偏好const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;return prefersDark ? "dark" : "light";};const [theme, setThemeState] = useState<Theme>(getInitialTheme);/** 设置主题(含保存与更新 DOM) */const setTheme = useCallback((newTheme: Theme) => {setThemeState(newTheme);document.documentElement.setAttribute("data-theme", newTheme);localStorage.setItem("theme", newTheme);}, []);/** 切换主题 */const toggleTheme = useCallback(() => {setTheme(theme === "light" ? "dark" : "light");}, [theme, setTheme]);/** 初始加载时同步 */useEffect(() => {document.documentElement.setAttribute("data-theme", theme);}, [theme]);//必须监听 theme,目的是同步 DOM 与 React 状态/** 监听系统主题变化 */useEffect(() => {const media = window.matchMedia("(prefers-color-scheme: dark)");const handleChange = (e: MediaQueryListEvent) => {const systemTheme: Theme = e.matches ? "dark" : "light";const savedTheme = localStorage.getItem("theme");// 仅当用户没有手动设置主题时,才跟随系统if (!savedTheme) {setTheme(systemTheme);}};media.addEventListener("change", handleChange);return () => media.removeEventListener("change", handleChange);}, [setTheme]);//如果未来 `setTheme` 改变引用(例如 ThemeProvider 重新创建),effect 会重新绑定监听,保持逻辑一致。return (<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>{children}</ThemeContext.Provider>);
};export const useTheme = (): ThemeContextType => {const context = useContext(ThemeContext);if (!context) throw new Error("useTheme must be used within a ThemeProvider");return context;
};

四、三种模式(跟随系统 / 亮色 / 暗色)的版本

import React, {createContext,useContext,useState,useEffect,useCallback,
} from "react";type ThemeMode = "light" | "dark" | "system";interface ThemeContextType {mode: ThemeMode; // 当前模式theme: "light" | "dark"; // 实际应用的主题setMode: (mode: ThemeMode) => void;toggleTheme: () => void;
}const ThemeContext = createContext<ThemeContextType | undefined>(undefined);export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({children,
}) => {const getInitialMode = (): ThemeMode => {const saved = localStorage.getItem("themeMode") as ThemeMode | null;return saved || "system";};const [mode, setModeState] = useState<ThemeMode>(getInitialMode);const [theme, setTheme] = useState<"light" | "dark">("light");/** 根据 mode 和系统设置决定最终主题 */const applyTheme = useCallback((currentMode: ThemeMode) => {if (currentMode === "light") {setTheme("light");document.documentElement.setAttribute("data-theme", "light");} else if (currentMode === "dark") {setTheme("dark");document.documentElement.setAttribute("data-theme", "dark");} else {const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;const systemTheme = prefersDark ? "dark" : "light";setTheme(systemTheme);document.documentElement.setAttribute("data-theme", systemTheme);}},[]);/** 设置模式(用户选择) */const setMode = useCallback((newMode: ThemeMode) => {setModeState(newMode);localStorage.setItem("themeMode", newMode);applyTheme(newMode);},[applyTheme]);/** 手动切换亮/暗模式(不影响系统模式) */const toggleTheme = useCallback(() => {if (mode === "system") {// 用户在系统模式下切换 → 固定到相反的主题(如果不做mode值区分,setMode("system" === "light" ? "dark" : "light");第一次切换一定是light)setMode(theme === "light" ? "dark" : "light");} else {setMode(mode === "light" ? "dark" : "light");}}, [mode, theme, setMode]);/** 初始化:同步主题 */useEffect(() => {applyTheme(mode);}, [mode, applyTheme]);/** 系统主题变化时,若为 system 模式则自动跟随 */useEffect(() => {const media = window.matchMedia("(prefers-color-scheme: dark)");const handleChange = (e: MediaQueryListEvent) => {if (mode === "system") {const systemTheme = e.matches ? "dark" : "light";setTheme(systemTheme);document.documentElement.setAttribute("data-theme", systemTheme);}};media.addEventListener("change", handleChange);return () => media.removeEventListener("change", handleChange);}, [mode]);return (<ThemeContext.Provider value={{ mode, theme, setMode, toggleTheme }}>{children}</ThemeContext.Provider>);
};export const useTheme = (): ThemeContextType => {const context = useContext(ThemeContext);if (!context)throw new Error("useTheme must be used within a ThemeProvider");return context;
};

App.tsx

import React from "react";
import { useTheme } from "./context/ThemeContext";const App: React.FC = () => {const { mode, theme, setMode, toggleTheme } = useTheme();return (<div style={{ padding: "2rem" }}><h1>🌗 React 三种主题模式</h1><p>当前模式:{mode}</p><p>当前实际主题:{theme}</p><div style={{ display: "flex", gap: "10px", marginBottom: "20px" }}><button onClick={() => setMode("light")}>亮色模式</button><button onClick={() => setMode("dark")}>暗色模式</button><button onClick={() => setMode("system")}>跟随系统</button><button onClick={toggleTheme}>手动切换主题</button></div><p>示例文字会根据主题自动变色。</p></div>);
};export default App;

五、其他说明

1. html[data-theme="dark"][data-theme="dark"] 区别

在这里插入图片描述

2.@importimport 区别

在这里插入图片描述

在这里插入图片描述

3.设置主题和切换主题为什么要用 useCallback?可以不用吗?

useCallback 是为了避免不必要的重新渲染。因为这两个函数会被传递给 Context,如果不使用 useCallback,每次组件重新渲染时都会创建新的函数,导致消费 Context 的子组件不必要的重新渲染。使用 useCallback 可以缓存函数,只有在依赖项变化时才更新函数。

使用 useCallback 的原因

useCallback 可以 缓存函数引用,避免每次渲染时都创建新函数(创建新函数会导致使用了useTheme()子组件重新渲染),防止因引用变化导致子组件不必要地重新渲染。

  1. 性能优化:避免不必要的子组件重渲染
  2. 依赖管理:在 useEffect 中作为依赖时保持引用稳定
  3. 最佳实践:对于传递给 context 的函数,保持稳定引用

👉 为什么有用?

  • 如果你的 ThemeContext 被很多组件订阅,
    那么没有 useCallback 时,每次 ThemeProvider 重新渲染,
    toggleTheme / setTheme 都会是新的函数引用,
    导致所有 useTheme() 的组件都重渲染。

在性能敏感或 Context 使用广泛的项目中,useCallback 是非常推荐的。

⚙️ 如果不写 useCallback 会怎样?

实际上也不会出错,主题切换功能仍然能正常工作。
只是:

  • 每次 ThemeProvider 渲染都会创建新函数引用;
  • 所有用到 useTheme() 的组件都会检测到 context 值变化;
  • 这可能导致 性能浪费(小项目影响不大,大项目明显)

4.切换主题需要监听 setTheme 吗?

  1. 这里的setTheme是一个useCallback 生成的函数,并不是useState中改变状态的setState;同时useCallback监听依赖性变化是重新创建函数,而不是像useEffect一样去执行一遍!
  2. useCallback 的依赖数组是 [theme, setTheme]。这意味着当 themesetTheme 发生变化时,toggleTheme 函数会被重新创建
  3. 为什么需要依赖 theme
    因为函数内部使用了 theme 的值,如果不在依赖数组中包含 theme,那么当 theme 变化时,toggleTheme 函数闭包中的 theme 还是旧值,导致切换逻辑错误。
  4. 为什么需要依赖 setTheme
    因为 setTheme 是稳定的(比如用 useCallback 包装且依赖为空),但为了确保总是调用最新的 setTheme,将其作为依赖是安全的。
http://www.dtcms.com/a/523563.html

相关文章:

  • 彻底清理:Vue项目中移除static文件夹的完整指南
  • 【Linux网络】套接字Socket编程预备
  • day18_菜单查询 合并servlet
  • 算法总结篇(枚举-分治)
  • TCP pure ACK 的不可扩展性问题
  • Android16 Wifi打开到自动连接的主要日志过程分析介绍
  • 背包dp——动态规划
  • 找做柜子的网站中国芯片制造最新消息
  • 甘肃省临夏州建设局网站wordpress 未分类
  • 用 Excalidraw+cpolar 做会议协作,像素级还原实体白板体验
  • 使用C++开发Android .so库的优势与实践指南
  • Spring AOP:注解配置与XML配置双实战
  • 基于YOLO11深度学习的半导体晶圆外观缺陷检测系统【Python源码+Pyqt5界面+数据集+安装使用教程+训练代码】【附下载链接】
  • 笔记本电脑待机、睡眠与休眠模式的技术差异解析
  • 2025丨时间很快,又来到1024
  • 基于python人脸识别系统 人脸检测 实时检测 深度学习 Dlib库 ResNet深度卷积神经网络 pyqt设计 大数据(源码)✅
  • 【C + +】unordered_set 和 unordered_map 的用法、区别、性能全解析
  • 《刚刚问世》系列初窥篇-Java+Playwright自动化测试-31- 操作日历时间控件-上篇(详细教程)
  • 电子商城网站建设与维护怎么建设淘客自己的网站_
  • 基于 Vue3 + WebSocket 实现的平板控制端与大屏展示端联动方案
  • 提高自己的网站网站 利润
  • 外贸seo软文发布平台上海百度推广优化排名
  • Qt 图像与定时器实战:实现动态图片轮播效果
  • C++ 模板初阶:从函数重载到泛型编程的优雅过渡
  • 第 01 天:Linux 是什么?内核、发行版及其生态系统
  • Docker 安装 MongoDB 完整指南:从入门到实战
  • Docker 离线安装
  • CUDA和cuDNN安装
  • 一篇初识什么是容器,引出 Docker
  • HTML 理论笔记