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

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

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

1️⃣ CSS变量方案(推荐,简单易维护) 适合需要动态主题切换和性能要求高的项目;

2️⃣ CSS-in-JS(如 styled-components、Emotion)方案

3️⃣ UI库自带方案(如 Ant Design、MUI 的 ThemeProvider)

一、总体思路

全局主题切换的核心是:

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

目录结构

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

二、实现步骤

1️⃣ 定义全局主题变量

创建 themes/dark.css

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

创建 themes/light.css

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

创建 src/styles/globals.css

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中:

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

tsx 复制代码
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

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️⃣在组件中使用主题切换

tsx 复制代码
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 变量样式

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

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

系统主题检测说明

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

src/context/ThemeContext.tsx

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;
};

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

tsx 复制代码
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

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,将其作为依赖是安全的。
相关推荐
木易 士心3 小时前
CSS 样式用法大全
前端·css·1024程序员节
wanhengidc3 小时前
云手机是一种应用软件吗?
运维·服务器·网络·游戏·智能手机·1024程序员节
恶猫3 小时前
_撸猫websocket服务器端,手机远程服务端
远程控制·aardio·autojs·1024程序员节·远控·手机远控·批量控制
WaWaJie_Ngen4 小时前
【设计模式】组合模式(Composite)
设计模式·组合模式·1024程序员节
CN.LG4 小时前
C# 企业微信机器人消息推送
c#·企业微信·1024程序员节·机器人推送
嵌入式-老费4 小时前
Easyx图形库应用(工业自动化领域的应用)
运维·自动化·1024程序员节
小Mei数码说4 小时前
华为Watch GT 6:运动与科技的完美融合
1024程序员节
比奥利奥还傲.4 小时前
不用服务器也能搭博客!Docsify+cpolar的极简方案
1024程序员节
YC运维4 小时前
ELK日志分析系统完整部署与应用指南
1024程序员节