第二章、全局配置项目主题色(主题切换+跟随系统)
全局配置项目主题色(主题切换)
在 React + TypeScript 项目中实现 全局主题切换(暗黑/亮色模式) 通常有三种常见方案:
1️⃣ CSS变量方案(推荐,简单易维护) 适合需要动态主题切换和性能要求高的项目;
2️⃣ CSS-in-JS(如 styled-components、Emotion)方案
3️⃣ UI库自带方案(如 Ant Design、MUI 的 ThemeProvider)
一、总体思路
全局主题切换的核心是:
不重新渲染页面,通过切换
data-theme或class来控制 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 中控制主题
实现思路
- 定义一个
ThemeContext,保存当前主题(light/dark)和修改函数。 - 用
ThemeProvider组件包裹整个应用。 - 在任何组件中通过
useContext获取当前主题和切换方法。 - 通过设置
<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读取上次主题。 -
点击按钮后会:
- 修改 React state
- 更新
<html data-theme="dark"> - 自动切换 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.@import 和 import 区别


3.设置主题和切换主题为什么要用 useCallback?可以不用吗?
useCallback 是为了避免不必要的重新渲染。因为这两个函数会被传递给 Context,如果不使用 useCallback,每次组件重新渲染时都会创建新的函数,导致消费 Context 的子组件不必要的重新渲染。使用 useCallback 可以缓存函数,只有在依赖项变化时才更新函数。
使用 useCallback 的原因:
useCallback 可以 缓存函数引用,避免每次渲染时都创建新函数(创建新函数会导致使用了useTheme()子组件重新渲染),防止因引用变化导致子组件不必要地重新渲染。
- 性能优化:避免不必要的子组件重渲染
- 依赖管理:在 useEffect 中作为依赖时保持引用稳定
- 最佳实践:对于传递给 context 的函数,保持稳定引用
👉 为什么有用?
- 如果你的
ThemeContext被很多组件订阅,
那么没有useCallback时,每次ThemeProvider重新渲染,
toggleTheme/setTheme都会是新的函数引用,
导致所有useTheme()的组件都重渲染。
在性能敏感或 Context 使用广泛的项目中,
useCallback是非常推荐的。
⚙️ 如果不写 useCallback 会怎样?
实际上也不会出错,主题切换功能仍然能正常工作。
只是:
- 每次
ThemeProvider渲染都会创建新函数引用; - 所有用到
useTheme()的组件都会检测到 context 值变化; - 这可能导致 性能浪费(小项目影响不大,大项目明显) 。
4.切换主题需要监听 setTheme 吗?
- 这里的
setTheme是一个useCallback生成的函数,并不是useState中改变状态的setState;同时useCallback监听依赖性变化是重新创建函数,而不是像useEffect一样去执行一遍! useCallback的依赖数组是[theme, setTheme]。这意味着当theme或setTheme发生变化时,toggleTheme函数会被重新创建。- 为什么需要依赖
theme?
因为函数内部使用了theme的值,如果不在依赖数组中包含theme,那么当theme变化时,toggleTheme函数闭包中的theme还是旧值,导致切换逻辑错误。 - 为什么需要依赖
setTheme?
因为setTheme是稳定的(比如用 useCallback 包装且依赖为空),但为了确保总是调用最新的setTheme,将其作为依赖是安全的。
