有些属性是应用程序中许多组件都需要的(如:地区偏好、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)组件。
- provider 组件通过
value属性提供 context 值。 - 消费组件,即订阅了 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 要展示的名字。
扩展使用
- context 值可以动态变化。
- provider 组件可以提供一个更新函数,让消费组件修改 context 中的数据。
- 为确保 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 能让你将这些数据沿着组件树向下广播,所有组件都能访问到这些数据,也能访问到后续的数据更新。