Kodokon kodokon.com

Context:无需逐层传递即可共享

用 context 传递数据而无需 prop drilling,同时把它的重新渲染和局限性控制在手中。

8 分钟 · 3 题

在 Kodokon 中打开本课

Prop drilling(逐层传 prop)指的是那些穿过三四个中间组件却在那里根本用不到、只是为了抵达树的某个叶子节点的 prop。它们经过的每一层都被耦合上了自己毫不关心的数据。context 解决的正是这个传输问题:createContext 创建通道,一个 provider 发布一个值,useContext 从任意后代读取它。要记住一个细微差别:context 传输状态,它并不管理状态——值仍然存在于某个组件里,这里就是 provider。

JSX
import { createContext, useMemo, useState } from "react";

const ThemeContext = createContext(null);

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("dark");
  const value = useMemo(
    () => ({ theme, setTheme }),
    [theme]
  );

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}
发布的值被 memo 化,以保持其引用稳定。

当 provider 的值改变时,所有消费者都会重新渲染,即便是嵌套很深的——而且 React.memo 保护不了它们,因为它比较的是 prop,不是 context。经典陷阱让情况雪上加霜:写 value={{ theme, setTheme }} 会在 provider 每次渲染时创建一个新对象,于是产生新引用,于是即便 theme 没变也会级联式重新渲染。上面的 useMemo 就是为此而生。再配上一个在 provider 之外会大声报错的访问 hook,整套设置就完整了。

JSX
import { useContext } from "react";

function useTheme() {
  const context = useContext(ThemeContext);
  if (context === null) {
    throw new Error(
      "useTheme must be inside <ThemeProvider>"
    );
  }
  return context;
}
一个明确的错误胜过一个悄无声息的 null。

在创建 context 之前,先把组合(composition)这个选项用尽。如果某个中间层除了转发一个 prop 之外什么都不做,就把结构反过来:在树的顶端、数据所在的地方构建好元素,再通过 children 或专门的 prop 往下传。这样中间层就变得通用了,数据也不再经过任何用不到它的组件。

JSX
function App({ currentUser }) {
  return (
    <Layout
      header={<TopBar user={currentUser} />}
      content={<Dashboard />}
    />
  );
}
Layout 对 currentUser 一无所知:它只是嵌入现成的元素。

知识检测

确认你已牢记本课的重点内容。

  1. 一个 context 消费者被包在 React.memo 里,而 context 的值改变了。会发生什么?
    • 它不会重新渲染:React.memo 阻断了所有更新
    • 它会重新渲染:React.memo 比较的是 prop,不是 context 的值
    • 只有当它的 prop 也改变时它才会重新渲染
  2. 为什么要对 provider 中传给 value 的对象做 memo 化?
    • 为了加快每次渲染时对象的创建
    • 因为 provider 要求一个不可变的值
    • 让引用只在数据改变时才改变,从而避免消费者不必要的重新渲染
  3. 对于更新极其频繁的状态,context 的主要局限是什么?
    • 没有选择器:每次更新都会重新渲染所有消费者,即便有些只用到值的一部分
    • context 是异步的,会延迟更新
    • context 只能存储原始类型的值