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
所不能的。