Pressidian
花园入口
笔记
项目
关于
实验室
GitHub
花园入口
笔记
项目
关于
实验室
GitHub

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/面试/八股/React/Hooks

useTheme

4 分钟阅读 · Note

目录树 578 篇

              • 一、ref 本身是什么
              • 自定义Hooks
              • React Hooks 面试金句与原理
              • useContext
              • useDebounce
              • useForm
              • useHandler
              • useList
              • useLocalStorage
              • useMemo的安全数组
              • useQuery
              • useTheme
            • 定时器组件 &ref与state⌚️
            • 生命周期 ⌚️
            • React
            • React 组件卸载与 this引用
          • 进阶篇题目列表
          • DOM&浏览器 API
          • HTTP&网络
          • TS
          • Vue
        • 可投递企业
      • 前端技术栈
    • 笔记目录
    • CLAUDE.md
    • Vue 组件与 Render 函数

关联笔记 6

↗一、ref 本身是什么同一路径↗自定义Hooks同一路径↗React Hooks 面试金句与原理同一路径↗useContext同一路径↗useDebounce同一路径↗useForm同一路径
  • useTheme

useTheme

当然。你只要真正理解一句话,useContext 就掌握一半了:

> useContext 用来让某个祖先组件,把数据直接提供给后代组件,避免一层一层通过 props 传递。

可以把 Context 想象成组件树里的一座“局部广播站”:

  • createContext:创建频道
  • Provider:广播数据
  • useContext:接收数据

一、为什么需要 useContext

假设组件结构是:

App
└── Layout
    └── Header
        └── UserAvatar

UserAvatar 需要当前用户信息。

如果只使用 props,就需要这样传递:

function App() {
  const user = { name: "Dano" };

  return <Layout user={user} />;
}

function Layout({ user }) {
  return <Header user={user} />;
}

function Header({ user }) {
  return <UserAvatar user={user} />;
}

function UserAvatar({ user }) {
  return <div>{user.name}</div>;
}

问题是:Layout 和 Header 自己根本不用 user,却必须负责转交。

这叫做 props drilling(属性逐层传递)。

使用 Context 后,可以让 App 直接把数据提供给下面所有组件。


二、useContext 的三个步骤

第一步:创建 Context

import { createContext } from "react";

const UserContext = createContext(null);

这里相当于创建了一个叫 UserContext 的数据频道。

注意:UserContext 本身不是具体数据,它只是负责标识“这是哪一种上下文”。


第二步:使用 Provider 提供数据

function App() {
  const user = {
    name: "Dano",
    age: 18,
  };

  return (
    <UserContext.Provider value={user}>
      <Layout />
    </UserContext.Provider>
  );
}

value={user} 就是实际广播出去的数据。

只要组件位于 UserContext.Provider 内部,不管嵌套多深,都可以读取 user。


第三步:使用 useContext 读取数据

import { useContext } from "react";

function UserAvatar() {
  const user = useContext(UserContext);

  return <div>当前用户:{user.name}</div>;
}

完整代码如下:

import { createContext, useContext } from "react";

const UserContext = createContext(null);

export default function App() {
  const user = {
    name: "Dano",
    age: 18,
  };

  return (
    <UserContext.Provider value={user}>
      <Layout />
    </UserContext.Provider>
  );
}

function Layout() {
  return <Header />;
}

function Header() {
  return <UserAvatar />;
}

function UserAvatar() {
  const user = useContext(UserContext);

  return (
    <div>
      {user.name},今年 {user.age} 岁
    </div>
  );
}

数据流是:

UserContext.Provider
        │ value={user}
        ▼
      Layout
        ▼
      Header
        ▼
  UserAvatar
        │
        └── useContext(UserContext)

三、Context 中的数据怎么修改

useContext 负责读取数据,但它自己并不管理数据。

如果数据需要变化,通常要配合 useState:

import {
  createContext,
  useContext,
  useState,
  type ReactNode,
} from "react";

type Theme = "light" | "dark";

interface ThemeContextValue {
  theme: Theme;
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextValue | null>(null);

export default function App() {
  const [theme, setTheme] = useState<Theme>("light");

  const toggleTheme = () => {
    setTheme(currentTheme =>
      currentTheme === "light" ? "dark" : "light"
    );
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <Page />
    </ThemeContext.Provider>
  );
}

function Page() {
  return <ThemeButton />;
}

function ThemeButton() {
  const context = useContext(ThemeContext);

  if (context === null) {
    throw new Error("ThemeButton 必须在 ThemeContext.Provider 内使用");
  }

  const { theme, toggleTheme } = context;

  return (
    <button onClick={toggleTheme}>
      当前主题:{theme}
    </button>
  );
}

这里的关键点是:

value={{ theme, toggleTheme }}

我们不仅把状态 theme 传下去了,也把修改状态的方法 toggleTheme 传下去了。

因此后代组件既能读取状态,也能修改状态。

当 theme 改变时,使用了这个 Context 的组件会自动重新渲染。React 会读取组件上方最近的 Provider,并订阅它的变化。React 官方 useContext 文档


四、推荐封装成自定义 Hook

每个组件都这样判断有点麻烦:

const context = useContext(ThemeContext);

if (context === null) {
  throw new Error("...");
}

实际项目通常封装一个 useTheme:

function useTheme() {
  const context = useContext(ThemeContext);

  if (context === null) {
    throw new Error("useTheme 必须在 ThemeProvider 内使用");
  }

  return context;
}

然后组件里直接使用:

function ThemeButton() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button onClick={toggleTheme}>
      当前主题:{theme}
    </button>
  );
}

还可以把 Provider 本身也封装起来:

import {
  createContext,
  useContext,
  useState,
  type ReactNode,
} from "react";

type Theme = "light" | "dark";

interface ThemeContextValue {
  theme: Theme;
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextValue | null>(null);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>("light");

  function toggleTheme() {
    setTheme(current =>
      current === "light" ? "dark" : "light"
    );
  }

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

export function useTheme() {
  const context = useContext(ThemeContext);

  if (context === null) {
    throw new Error("useTheme 必须在 ThemeProvider 内使用");
  }

  return context;
}

在入口处包裹:

function App() {
  return (
    <ThemeProvider>
      <Page />
    </ThemeProvider>
  );
}

任意后代组件使用:

function Page() {
  const { theme, toggleTheme } = useTheme();

  return (
    <main>
      <p>当前主题:{theme}</p>
      <button onClick={toggleTheme}>切换主题</button>
    </main>
  );
}

这是实际开发中最推荐的组织方式。


五、createContext 里的默认值是什么

例如:

const ThemeContext = createContext("light");

如果组件上方没有对应的 Provider:

function Button() {
  const theme = useContext(ThemeContext);

  console.log(theme); // "light"
}

它会得到 createContext 中的默认值。

但要注意:

<ThemeContext.Provider value={undefined}>
  <Button />
</ThemeContext.Provider>

这时拿到的是 undefined,不是默认值。

默认值只在“完全找不到 Provider”时生效。

在 TypeScript 项目中,我通常推荐:

const ThemeContext = createContext<ThemeContextValue | null>(null);

然后通过自定义 Hook 检查:

if (context === null) {
  throw new Error("useTheme 必须在 ThemeProvider 内使用");
}

这样如果忘记包裹 Provider,错误会非常明确。


六、多个 Provider 时读取哪一个

useContext 会读取组件上方距离它最近的 Provider:

<ThemeContext.Provider value="light">
  <Header />

  <ThemeContext.Provider value="dark">
    <Content />
  </ThemeContext.Provider>
</ThemeContext.Provider>

结果:

function Header() {
  const theme = useContext(ThemeContext);
  // "light"
}

function Content() {
  const theme = useContext(ThemeContext);
  // "dark"
}

内层 Provider 会覆盖外层 Provider,但只影响自己的后代。

这很像 JavaScript 中的作用域:

const theme = "light";

{
  const theme = "dark";
  console.log(theme); // "dark"
}

七、一个特别容易犯的错误

下面这样不行:

function App() {
  const theme = useContext(ThemeContext);

  return (
    <ThemeContext.Provider value="dark">
      <div>{theme}</div>
    </ThemeContext.Provider>
  );
}

因为 App 调用 useContext 时,Provider 在它返回的 JSX 里面,不在 App 的上方。

Context 只能向下提供,不能提供给 Provider 自己所在的组件。

应该拆成:

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Content />
    </ThemeContext.Provider>
  );
}

function Content() {
  const theme = useContext(ThemeContext);

  return <div>{theme}</div>;
}

八、什么时候应该使用 Context

适合放入 Context 的数据通常具有两个特点:

  1. 很多不同层级的组件都需要。
  2. 数据具有一定的“全局性”或“区域共享性”。

常见场景:

  • 当前登录用户
  • 主题色、深色模式
  • 当前语言
  • 用户权限
  • 全局弹窗
  • 表单多个子组件共享的数据
  • 页面级配置

不建议什么都放 Context。

例如只有父子两层需要的数据:

function Parent() {
  const name = "Dano";

  return <Child name={name} />;
}

直接传 props 更简单,不需要 Context。

可以这样判断:

> 数据只传一两层:优先 props。 > 很多层、很多组件都需要:考虑 Context。


九、性能方面要注意什么

当 Provider 的 value 发生变化时,读取该 Context 的组件会重新渲染。

下面每次 App 渲染都会产生一个新对象:

<UserContext.Provider value={{ user, logout }}>
  <Page />
</UserContext.Provider>

即使 user 没变化,{ user, logout } 也是一个新的对象引用。

组件较复杂时,可以用 useMemo 和 useCallback 保持引用稳定:

const logout = useCallback(() => {
  setUser(null);
}, []);

const contextValue = useMemo(() => {
  return {
    user,
    logout,
  };
}, [user, logout]);

return (
  <UserContext.Provider value={contextValue}>
    <Page />
  </UserContext.Provider>
);

不过不要一看到 Context 就急着优化。先正常编写,确认存在实际性能问题后再处理。

还有一种常见优化方式是把不同类型的数据拆成多个 Context:

<UserContext.Provider value={user}>
  <ThemeContext.Provider value={theme}>
    <App />
  </ThemeContext.Provider>
</UserContext.Provider>

这样主题改变时,不会因为把所有数据塞在同一个 Context 中而影响不相关的用户组件。


十、React 19 的写法变化

为了兼容 React 18 及更早版本,我们前面使用的是:

<ThemeContext.Provider value={theme}>
  <Page />
</ThemeContext.Provider>

React 19 开始,可以简写为:

<ThemeContext value={theme}>
  <Page />
</ThemeContext>

两者表达的意思相同;React 18 项目继续使用 .Provider。React 官方 createContext 文档


最后总结

记住这个固定套路:

// 1. 创建
const MyContext = createContext(defaultValue);

// 2. 提供
<MyContext.Provider value={数据}>
  <Child />
</MyContext.Provider>

// 3. 读取
const 数据 = useContext(MyContext);

如果数据需要修改:

const [state, setState] = useState(...);

<MyContext.Provider value={{ state, setState }}>
  <Child />
</MyContext.Provider>

你可以把它牢牢记成一句话:

> useState 负责保存数据,Provider 负责向下提供数据,useContext 负责读取并订阅数据。