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

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

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,将其作为依赖是安全的。
相关推荐
im_AMBER11 分钟前
React 18
前端·javascript·笔记·学习·react.js·前端框架
老前端的功夫13 分钟前
Vue2中key的深度解析:Diff算法的性能优化之道
前端·javascript·vue.js·算法·性能优化
集成显卡36 分钟前
AI取名大师 | PM2 部署 Bun.js 应用及配置 Let‘s Encrypt 免费 HTTPS 证书
开发语言·javascript·人工智能
han_37 分钟前
前端高频面试题之Vue(高级篇)
前端·vue.js·面试
不说别的就是很菜1 小时前
【前端面试】CSS篇
前端·css·面试
by__csdn2 小时前
nvm安装部分node版本后没有npm的问题(14及以下版本)
前端·npm·node.js
Dm_dotnet2 小时前
React:使用Tailwind CSS、Streamdown与Ant Design X
react.js
by__csdn2 小时前
Node与Npm国内最新镜像配置(淘宝镜像/清华大学镜像)
前端·npm·node.js
脸大是真的好~2 小时前
黑马JAVAWeb -Vue工程化-API风格 - 组合式API
前端·javascript·vue.js
我命由我123452 小时前
CesiumJS 案例 P35:添加图片图层(添加图片数据)
开发语言·前端·javascript·css·html·html5·js