拥抱React-Hooks(三)- useContext

Prop Drilling 问题


  • React 应用趋于复杂,往往会形成深层组件树。
  • 父子组件通过 props 传递参数来通信。
  • React 是单向数据流。

基于上述三点,就容易产生这样的问题:父组件要逐层把一些 props 传递给目标子组件,而中间子组件并不关心这些props,却要负责透传这些 props ,还要确保中间不出问题。

这就是 ReactProp 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 也可以嵌套使用,里层的会覆盖外层的数据。
  • Providervalue 值发生变化时,它内部的所有 Consume 组件都会重新渲染。且都不受制于shouldComponentUpdate 函数。

Class.contextType

这是一种支持 class组件的,更灵活的一种消费单个 contextAPI

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 的函数作为一个对象,赋于 providervalue。目标子组件可以读取父组件共享的动态数据,并在数据改变的时候更新它。这相当于把修改后数据回传给了父组件。

多个 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 ContextAPI 也可以在函数组件中使用,为什么还需要 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 所不能的。


参考资料

官网-Context
React Context