React 学习笔记(3)—— Context

113 阅读4分钟

有些属性是应用程序中许多组件都需要的(如:地区偏好、UI 主题、当前用户、语言等),对于自上而下的数据流而言,实现这些属性非常繁琐。而 Context 可以很好地解决这一问题。使用 Context,无需为每层组件手动添加 props,就能在组件树中传递数据。

Context 使用方式

class 组件写法:

import React from "react";
import type { ContextType } from "react";
import { Button } from "antd";
import type { ButtonProps } from "antd";

const SizeContext = React.createContext<ButtonProps["size"]>("small");

class App extends React.Component {
  render() {
    return (
      <SizeContext.Provider value="small">
        <Toolbar />
      </SizeContext.Provider>
    );
  }
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  context: ContextType<typeof SizeContext>;

  render() {
    return <Button size={this.context}></Button>;
  }

  static contextType = SizeContext;
}

函数组件写法:

import React, { useContext } from "react";
import { Button } from "antd";
import type { ButtonProps } from "antd";

const SizeContext = React.createContext<ButtonProps["size"]>("small");

function App() {
  return (
    <SizeContext.Provider value="small">
      <Toolbar />
    </SizeContext.Provider>
  );
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const size = useContext(SizeContext);
  return <Button size={size}></Button>;
}

Context 的使用,分为两部分:provider 组件消费(consumer)组件

  1. provider 组件通过 value 属性提供 context 值。
  2. 消费组件,即订阅了 Context 的组件,通过 this.context(class 组件)或是 useContext(函数组件)。

两者间通过 Context 对象来联系。消费组件会沿着组件树向上寻找所订阅的、离自身最近的 context。

API

React.createContext

const MyContext = React.createContext(defaultValue);

创建一个 Context 对象,并提供默认值 defaultValue。只有组件树中没有找到匹配的 provider 时,defaultValue 参数才会生效。默认值有助于对没有 provider 包装的组件进行测试。

Context.Provider

每个 Context 对象都会返回一个 provider 组件,允许消费组件订阅 context 的变化。

provider 接收一个 value 属性,传递给消费组件。provider 可以嵌套使用。

provider 的 value 值变化时,内部的所有消费组件都会重新渲染,从 provider 到内部消费组件的传播不受制于 shouldComponentUpdate 函数,因此当消费组件在其祖先组件跳过更新情况下也能更新。

provider 的 value 值通过新旧值检测确定变化,使用了和 Object.is 相同的算法。

Class.contextType

class 组件的静态属性 contextType 用于指定 Context 对象,使得组件可以通过 this.context 属性获取 context 值。

Context.Consumer 和 useContext

Context.Consumer 组件可用于在函数组件中订阅 Context。子元素是一个函数,函数接收当前的 context 值,并返回一个 React 元素。

实际上,函数组件通常使用 useContext Hook 来订阅 Context。

Context.displayName

Context 对象接受一个名为 displayName 的字符串属性,React DevTools 使用该字符串作为 Context 要展示的名字。

扩展使用

  1. context 值可以动态变化。
  2. provider 组件可以提供一个更新函数,让消费组件修改 context 中的数据。
  3. 为确保 context 快速渲染,React 需要每个 context 在组件树中单独作为一个节点。在函数组件中,不管使用 useContext 还是 Context.Consumer 都可以同时订阅多个 Context。

注意事项

Context 意外渲染

由于 Context 会根据引用决定何时渲染,因此,当 provider 组件的父组件重新渲染时,可能会在消费组件中触发意外渲染。使用 state 保存 context 的值可以避免这一问题。

function App() {
  const [value] = useState({ something: "something" });

  return (
    <MyContext.Provider value={value}>
      <Toolbar />
    </MyContext.Provider>
  );
}

是否需要使用 Context

Context 的设计目的是为了共享那些相对于组件树的全局数据。

Context 主要应用场景在于——很多不同层级的组件需要访问某些相同的数据。使用时应谨慎,因为这会使组件的复用性变差。

组件组合(component composition)也是一种避免层层传递属性的方法。将深层组件在顶层组件中生成,并直接将深层组件作为属性向下传递,这样做也可以避免层层传递过多属性的问题,同时也为后续扩充属性预留一定空间。

import React from "react";

function Page({ link }) {
  const userLink = <a href={link} />;
  return <PageLayout userLink={userLink} />;
}

function PageLayout({ userLink }) {
  return <NavigationBar userLink={userLink} />;
}

function NavigationBar({ userLink }) {
  return <div>{userLink}</div>;
}

这种对组件的控制反转减少了应用中 props 的数量,在很多场景下会使代码更加干净,并且对根组件有更多的把控。但这种把逻辑提升到组件树的更高层次来处理的做法,也可能使得高层组件变得更复杂。

这种模式可以覆盖很多场景,在这些场景中子组件和直接关联的父组件是解耦的。但是,如果组件树中很多不同层级的组件都需要访问同样的一批数据,Context 能让你将这些数据沿着组件树向下广播,所有组件都能访问到这些数据,也能访问到后续的数据更新。