Prop Drilling 问题
- 当
React应用趋于复杂,往往会形成深层组件树。 - 父子组件通过
props传递参数来通信。 React是单向数据流。
基于上述三点,就容易产生这样的问题:父组件要逐层把一些 props 传递给目标子组件,而中间子组件并不关心这些props,却要负责透传这些 props ,还要确保中间不出问题。
这就是 React 中 Prop Drilling 问题。
+----------------+
| |
| A |
| |Props |
| v |
| |
+--------+-------+
|
+---------+-----------+
| |
| |
+--------+-------+ +--------+-------+
| | | |
| | | + |
| B | | |Props |
| | | v |
| | | |
+----------------+ +--------+-------+
|
+--------+-------+
| |
| + |
| |Props |
| v |
| |
+--------+-------+
|
+--------+-------+
| |
| + |
| |Props |
| C |
| |
+----------------+
Context
React 16.3.0 中引入了 Context 系列 API。用于共享那些对于一个组件树而言是 “全局” 的 state。可用于解决 prop drilling 问题。但会导致组件复用性降低,忌滥用。
Context 的设计基于 Provider-Consumer 模式,包含一系列 API。
基础 API
React.createContext(defaultValue): 创建一个Context对象。Context.Provider: 你创建的Context对象的组件,用于 初始化 共享数据的组件。Context.Consumer: 你创建的Context对象的组件,用于 获取 共享数据的组件。
我们来通过示例来看一下这些 API :
// ./MyContext.jsx
import React from "react";
// 创建 context 对象
const MyContext = React.createContext("hello");
// Devtools 中显示用
MyContext.displayName = 'MyDisplayName';
export default MyContext
import React from "react";
// 引入 context 对象
import MyContext from "./MyContext";
// 父组件(Provider)
// 使用 context 对象的 Provider 组件共享数据
const A = () => (
<>
<MyContext.Provider value="hello context">
<B />
</MyContext.Provider>
<DD/>
</>
);
// B C 为 中间组件
const B = () => <C />;
const C = () => <D />;
// 目标子组件 - D
// 使用 context 对象的 Consumer 组件获取共享数据
const D = () => (
<MyContext.Consumer>
{value => <input type="text" value={value} />}
</MyContext.Consumer>
);
// 没有被 Provider 组件包裹的子组件 - DD
// value 的值为 `hello` 即context创建时的默认值
const DD= () => (
<MyContext.Consumer>
{value => <input type="text" value={value} />}
</MyContext.Consumer>
);
export default A;
- 使用
React.createContext("hello")创建了一个context对象 -MyContext。 - 使用
MyContext对象的Provider组件来共享数据。数据赋值给value属性,可以是复杂数据。被Provider组件包裹的子组件链才能获取value中数据。 - 使用
MyContext对象的Consumer组件来获取共享数据。获取数据是通过内部的一个回调函数,回调函数的参数就是共享的数据。 DD子组件没有在Provider包裹的组件链中,使用Consumer组件只能获取到定义context时候的默认值 -hello。
+----------------+
| |
| A |
| |
| Provide |
| Context |
+--------+-------+
|
+---------+-----------+
| |
| |
+--------+-------+ +--------+-------+
| | | |
| | | |
| DD | | B |
| | | |
| | | |
+----------------+ +--------+-------+
|
+--------+-------+
| |
| |
| C |
| |
| |
+--------+-------+
|
+--------+-------+
| |
| D |
| |
| Consume |
| Context |
+----------------+
其他须知:
- 多个
Provider也可以嵌套使用,里层的会覆盖外层的数据。 - 当
Provider的value值发生变化时,它内部的所有Consume组件都会重新渲染。且都不受制于shouldComponentUpdate函数。
Class.contextType
这是一种仅支持 class组件的,更灵活的一种消费单个 context 的 API:
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* 基于 MyContext 组件的值进行渲染 */
}
}
MyClass.contextType = MyContext;
挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
复杂场景
- 动态 & 可更新
Context - 多个
Context
动态 & 可更新 Context
对于复杂交互的深层嵌套组件树,其 Prop Drilling 问题可能更严重 - 数据双向传递。即父组件把初始数据通过 props 层层传递到目标子组件。目标子组件会改变数据,再通过 callback 层层传递给父组件来统一提交。
对于这种个别情况,你也不想因此引入 redux。那么,你可以通过 Context 来解决。Context 可以是动态的,可更新的。
来看下面示例:
// 创建context
// 确保传递给 createContext 的默认值数据结构是调用的组件(consumers)所能匹配的!
export default MyContext = React.createContext({
value: 'hello',
setValue: () => {},
});
// 父组件
import React from "react";
import MyContext from "./MyContext";
class A extends React.Component {
constructor(props) {
super(props);
this.setValue = newValue => {
this.setState({ value: newValue });
};
// State 也包含了更新函数,因此它会被传递进 context provider。
this.state = {
value: "Hello context",
setValue: this.setValue
};
}
render() {
return (
<>
<MyContext.Provider value={this.state}>
<B />
</MyContext.Provider>
<p>{this.state.value}</p>
</>
);
}
}
// B C 为 中间组件
const B = () => <C />;
const C = () => <D />;
// 目标子组件 - D
// 使用 context 对象的 Consumer 组件获取共享数据,通过更新函数更新数据
const D = () => (
<MyContext.Consumer>
{({ value, setValue }) => (
<input
type="text"
value={value}
onChange={e => {
setValue(e.target.value);
}}
/>
)}
</MyContext.Consumer>
);
export default A;
我们把父组件本地 state,和用于更新 state 的函数作为一个对象,赋于 provider 的 value。目标子组件可以读取父组件共享的动态数据,并在数据改变的时候更新它。这相当于把修改后数据回传给了父组件。
多个 Context
当共享数据较多,不同的子组件需要的数据也不一样。我们可以考虑创建多个 context。而其中某个组件需要不止一个 context 共享数据的时候,它可以消费多个 context:
// 一个组件通过Consumer组件嵌套,消费多个 context 👇
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
useContext hook
既然 React Context 的 API 也可以在函数组件中使用,为什么还需要 useContext Hook 呢?
前面我们了解到,子组件要消费 context 需要通过 Consumer 组件包装。要消费多个 context,还需要嵌套 Consumer 组件。useContext Hook 只是新增了一种方式,让你在函数组件中更方便,更优雅的消费 context。创建 和 Provider 方式不变。
前面 动态&更新context 的示例,目标子组件使用 useContext Hook 消费 context 代码如下:
//
// 目标子组件 - D
// 直接使用 useContext 消费共享数据, 并通过更新函数更新数据。
const D = () => (
const { value, setValue } = React.useContext(MyContext)
return (
<input
type="text"
value={value}
onChange={e => {
setValue(e.target.value);
}}
/>
)
);
可读性更高了,还可很方便的消费多个 context,这是 class组件 的 contextType 所不能的。