React 进阶 -- Context

在 React 中数据总是单向传递的,如果某个属性许多组件都需要就会使该过程变得极其繁琐,Context 提供了在组件间共享此类数据的方式,而不必显式地通过组件树的逐层传递 props。

什么情况下使用 Context

  1. 当某个(些)属性需要传递的层级很深时

    考虑下面的例子:

    class App extends Component {
      state = { count: 1 }
      render() {
        return (
            <div className='App'>
              <h1>这是根组件</h1>
              <Father count={this.state.count} />
            </div>
        );
      }
    }
    
    class Father extends Component {
      render() {
        return (
          <div className='father'>
            <h1>这是父组件</h1>
            <Son count={this.props.count} />
          </div>
        );
      }
    }
    
    class Son extends Component {
      render() {
        return (
          <div className='son'>
            <h1>这是子组件</h1>
          	<h2>{this.props.count}</h2>
          </div>
        );
      }
    }
    

    如果 Son 组件需要来自根组件的 count,则需要从 App 组件开始通过props 属性自上而下传递给 Son 组件。使用 context, 我们可以避免通过中间元素传递 props:

    // Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
    // 为当前的 theme 创建一个 context(1为默认值)。
    const countContext = React.createContext(1);
    class App extends Component {
      state = { count: 1 };
      render() {
        return (
         // 使用一个 Provider 来将当前的 state 传递给以下的组件树。
        // 无论多深,任何组件都能读取这个值。
        // 在这个例子中,我们将 state 作为当前的值传递下去。
          <Provider value={this.state}>
            <div className='App'>
              <h1>这是根组件</h1>
              <Father />
            </div>
          </Provider>
        );
      }
    }
    
    // 中间的组件再也不必指明往下传递了。
    class Father extends Component {
      render() {
        return (
          <div className='father'>
            <h1>这是父组件</h1>
            <Son />
          </div>
        );
      }
    }
    
    class Son extends Component {
      // 指定 contextType 读取当前的 count context。
      // React 会往上找到最近的 count Provider,然后使用它的值。
      // 在这个例子中,当前的 count 值为 1。
      static contextType = countContext; 
      render() {
        return (
          <div className='son'>
            <h1>这是子组件</h1>
            <h2>{this.context.count}</h2>
          </div>
        );
      }
      // 也可使用 Consumer 渲染出 context
      /*
      render() {
        const { Consumer } = globalContext;
        return (
          <div className='son'>
            <h1>这是子组件</h1>
            <h2>{this.context.count}</h2>
            <Consumer>{(context) => <h2>{context.count}</h2>}</Consumer>
            <button onClick={this.add}>点我加1</button>
            <button onClick={this.minus}>点我减1</button>
          </div>
        );
      }
      */
    }
    

    如何在 Son 组件中更改 Context 呢?

    // 导出 context 修改函数
    const actions = (self) => ({
      add() {
        self.setState((preState) => ({ count: preState.count + 1 }));
      },
      minus() {
        self.setState((preState) => ({ count: preState.count - 1 }));
      },
    });
    
    class App extends Component {
      // 扩展 actions 并传入 this
      state = { count: InitialContext.count, ...actions(this) };
      render() {
        return (
          <Provider value={this.state}>
            <div className='App'>
              <h1>这是根组件</h1>
              <Father />
            </div>
          </Provider>
        );
      }
    }
    
    class Son extends Component {
      static contextType = globalContext;
    
      // 调用在 context 中定义好的加减函数
      add = () => {
        this.context.add();
      };
    
      minus = () => {
        this.context.minus();
      };
    
      render() {
        return (
          <div className='son'>
            <h1>这是子组件</h1>
            <h2>{this.context.count}</h2>
            <button onClick={this.add}>点我加1</button>
            <button onClick={this.minus}>点我减1</button>
          </div>
        );
      }
    }
    

    Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。如果仅仅是为了避免层层传递属性可以使用组件组合

  2. 无亲属关系的组件需要共用的数据

    考虑下面的例子:

    class App extends Component {
      render() {
        return (
          <div className='App'>
            <h1>这是根组件</h1>
          </div>
        );
      }
    }
    
    class Test extends Component {
      render() {
        return (
          <div className='son'>
            <h1>这是另一个组件</h1>
          </div>
        );
      }
    }
    

    App 组件和 Test 组件既不是父子关系也不是兄弟关系,如果想要将 App组件中的数据传递给 Test 组件可以利用 Context 来实现:

    const countContext = React.createContext("我是要传递的数据");
    class App extends Component {
      render() {
        return (
          <div className='App'>
            <h1>这是根组件</h1>
          </div>
        );
      }
    }
    
    class Test extends Component {
      static contextType = globalContext;
    
      render() {
        return (
          <div className='test'>
            <h1>这是另一个组件</h1>
            <h2>{this.context}</h2>
          </div>
        );
      }
    }
    

注意事项

Context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 Provider 的父组件进行重渲染时,可能会在 Consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 Consumers 组件,因为 value 属性总是被赋值为新的对象:

class App extends Component {
  render() {
    return (
      <Provider value={1}>
        <div className='App'>
          <h1>这是根组件</h1>
          <Father />
        </div>
      </Provider>
    );
  }
}

因此,将 value 状态提升到父节点的 state 中是更好的做法