一文搞懂 useContext

974 阅读4分钟

React Context 提供了一个不用向每层组件手动添加 props,就能在组件树之间进行数据传递的方法。

为什么需要 Context

当孙子组件需要使用爷爷组件的变量时,需要在父组件上进行实际上无用的传递。React 数据一般通过 props 由父到子传递,context 提供了一种在组件间共享数据的方式,不用我们显示的层层传递数据。

使用 Context 之前的考虑

官方文档说到,context 会使组件的复用性变差,所以建议谨慎使用。但是如果我们在纯业务组件中并不会考虑复用性,这时候使用就没有任何问题。

考虑到 context 解决的问题,有时候可以使用组件组合。这种方式是要把需要使用数据的组件提升到高层次组件,然后将组件本身层层传递下去。但是将底层逻辑提升,首先会污染高层组件,使它更复杂,然后还具有传染性,后面的组件还是需要向下层层传递。而且被提升的组件需要和父组件完全解耦,限制也较多。

如何使用

创建 context

  const MyContext = React.createContext(defaultValue)

提供 context

  <MyContext.Provider value={}>
    {children}
  </MyContext.Provider>

消费 context

  // class contextType
  static contextType = MyContext;
  console.log(this.context)

  // function hook
  const value = useContext(MyContext)

  // 使用 consumer
  // Consumer 需要一个函数作为子元素。这个函数接受 context 的值作为参数,并返回一个 React 节点。
  <MyContext.Consumer>
    {value => <span>{value}</span>}
  </MyContext.Consumer>

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效(有点类似解构赋值时,只有在 undefined 下赋值才会生效)。

Provider 接收一个 value 属性,传递给消费组件。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。这是因为更新 value 值一般是使用 setState,会引发后代更新。从 Provider 到其后代 consumer 组件的传播,不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新,甚至 consumer 组件内部的 shouldComponentUpdate 返回了 false,也不影响它消费 context 进而重新渲染。

这里的所有消费组件指 Provider 的所有后代。可以想到,订阅了 context 的后代重新渲染很合理,那么没有订阅的也要重新渲染,是不是有点浪费呢,怎么避免这些无效的重复渲染呢?

  • 合理组织组件结构 将需要使用的消费组件放到 Provider 中,其它的放到外部。这种方式仅适用于根组件,假如在消费组件中存在未使用 context 的组件,还是会重新渲染。

  • 使用 pureComponent 或者 shouldComponentUpdate 可以避免 class 组件的更新,也不影响 context 继续向下传播,只适用于 class 组件。

  • 用一个组件包裹 Context.Provider,把子组件以 props.children 的方式传进来

      // context wrapper
      const MyContext = React.createContext('')
    
      const ProviderWrapper = (props) => {
        const [value, setValue] = useState('test')
        return (
          <MyContext.Provider value={value}>
            <button onClick={() => {setValue(+new Date())}}>修改 context</button>
            {props.children}
          </MyContext.Provider>
        )
      }
    
      // App 组件
      import ProviderWrapper from './providerWrapper'
    
      class App extends React.Component {
    
        render() {
          return (
            <ProviderWrapper>
              <ClassCompt2 />
              <ClassCompt3 />
            </ProviderWrapper>
          )
        }
      }
    

    以上例子,ClassCompt3 使用了 context ,ClassCompt2 未使用,使用上面这种方式,ClassCompt2 不会重新渲染。

    为什么呢?React 默认行为是,父组件 setState 时,如果子组件的 shouldComponentUpdate 返回 true(默认为 true),子组件就会重新渲染。常规代码如下:

      <MyContext.Provider value={value}>
        <ClassCompt2 />
        <ClassCompt3 />
      </MyContext.Provider>
    

    App 组件成了无状态组件,传递给 ProviderWrapper 的 children,在更新时没有发生改变,就不会再重新渲染了。

使用技巧

  • 深层组件更新 context context 中包含一个修改值的函数,底层组件可以通过调用这个函数修改 context 的值。

  • 同一个组件消费多个 context 函数组件使用多个 useContext,class 组件用函数包裹使用 Context.Consumer 嵌套包裹

      <ThemeContext.Consumer>
        {theme => (
          <UserContext.Consumer>
            {user => (
              <ProfilePage user={user} theme={theme} />
            )}
          </UserContext.Consumer>
        )}
      </ThemeContext.Consumer>
    
  • context 使用浅比较决定是否重渲染订阅的组件,假如使用下面的方式传入 value,会导致多余的重渲染

      <MyContext.Provider value={{value: 'something'}}>
        {children}
      </MyContext.Provider>
    

    由于每一次重渲染传给 value 属性的值都是一个全新的对象,会导致后代每次都会重渲染。所以最好把 value 值放到 state 里。

      constructor(props) {
        super(props);
        this.state = {
          value: {something: 'something'},
        };
      }
    
      <MyContext.Provider value={this.state.value}>
        {children}
      </MyContext.Provider>
    

总结

context 虽然有时候可以满足全局状态的概念,但是还有一些细节需要注意,正确的使用才能避免出现问题。