React Context 详解

本文详细介绍了React Context的使用场景、API、最佳实践及注意事项。阐述了何时使用和不适用Context,提供了Provider和Consumer组件的用法,并讨论了动态更新Context、消费多个Context以及避免渲染陷阱的策略。

React Context 是一个强大的组件广播功能,但是也不能随意滥用,它并不适合一些简单的组件通信,如果杀鸡用牛刀可能会起到相反的效果


什么时候使用

  1. 当你要在组件树中共享一些全局数据的时候,比如要让你的应用风格可配置,或则语言可选 那么像这种 themelanguage 的属性就可以托管给 Context

案例
先考虑不用context 的情况

// 假如现在你的组件树中有多个按钮需要随时切换背景颜色

// 你封装的按钮
class ThemeButton extends React.Component {
  render() {
    return <button style={{backgroundColor: this.props.theme}}>ThemeButton</button>
  }
}

// 在某个组件中使用了这个按钮
function Toolbar(props) {
  return (
    <div>
      <ThemeButton theme={props.theme}/>
    </div>
  )
}

// 为了能够在同一控制所有按钮的样式 你可以在App.js 中传入样式
class App extends Component {
  render() {
    return (
      <div>
        <Toolbar theme="blue"/>
      </div>
    );
  }
}

你会发现 就一个简单的theme 属性要通过 App -> Toolbar -> ThemeButton ,如果嵌套的关系更加复杂,那么这种传递props的方式就不太合适了

Context 广播 来优化

// 声明一下我将所有组件写在一起只是方便演示,你可以将组件写在不同的文件中
//1. 首先你可以在src 下创建一个context的文件并创建index.js
import React from 'react'

export const ThemeContext = React.createContext('green')

之后你就可以使用这个导出的ThemeContext

import {ThemeContext} from '../context'

class ThemeButton extends React.Component {
  static contextType = ThemeContext
  render() {
    return <button style={{backgroundColor: this.context}}>ThemeButton</button>
  }
}

// 在某个组件中使用了这个按钮
function Toolbar(props) {
  return (
    <div>
      <ThemeButton/>
    </div>
  )
}

// 为了能够在同一控制所有按钮的样式 你可以在App.js 中传入样式
class App extends Component {
  render() {
    return (
      <ThemeContext.Provider value="blue">
        <Toolbar/>
      </ThemeContext>
    );
  }
}

如果你手动敲一遍上述的案例,你将发现Theme不在通过props逐层传递,而是通过Context 广播的方式,


我们来看看 createContext 这个Api

createContext 是 React 内置的方法,该方法返回一个对象,对象拥有两个属性分别是 Provider 和 Consumer。 这两个属性都是React 组件,通过这两个组件可以管理广播的播放,和接收,通过上述的案例可以发现我们并没有使用 Consumer 这个组件 而只使用了Provider 所以我们先分析这个组件到底做了什么

  1. 首先你要用 Provider 包裹你的根组件,这样你的组件树都在广播的范围内了
  2. 定义广播的内容,Provider 接收一个value 的属性用来定义广播的内容,当然这个属性不是必须写的,默认广播的内容你可以直接传给 createContext 这个方法
  3. 在要订阅广播的组将中将 contextType 这个静态属性声明为你定义的上下文对象
  4. 之后你的类组件中将拥有context这个属性,组件会从组件树中离自身最近的那个匹配的 Provider 中读取值并赋给context,只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。
  5. 一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
  6. 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
  7. 你可以在订阅广播的组件中的任意生命周期中使用context 这个属性

其上就是广播的基本使用,案例中接收广播局限在类式组件中,除此之外Comsumer 还未使用过,那么想要在函数中接收广播,和了解Comsumer 的使用,请继续阅读本章。


什么时候不该使用

Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据, 不过对于一些复用性较强的组件还是老老实实使用props,如果你使用 Context 会极大的提高组件的耦合,降低组件的复用性。

解决方案是

  • 你可以在复用性强的组件的父级接收广播然后通过 props 传给该组件
  • 如果数据不是全局数据, 为了避免过多的props传递,同时又不想使用广播,那么你也可以在父级将底层的组件组装好后,通过props 向下传递。不过这种组件的控制反转会使得父级变得复杂,同时底层的组件将变得局限起来,具体如何使用还要根据情况考虑

Context.Consumer

回归上述没有解决的问题,如果你要在函数组件中订阅广播可以像如下这么做

案例

import {ThemeContext} from '../context'

function ThemeButton () {
  return (
    <ThemeContext.Consumer>
      {value=>(
        <button style={{backgroundColor: value}}>ThemeButton</button>
      )}
    </ThemeContext.Consumer>
  )
}

// 在某个组件中使用了这个按钮
function Toolbar(props) {
  return (
    <div>
      <ThemeButton/>
    </div>
  )
}

// 为了能够在同一控制所有按钮的样式 你可以在App.js 中传入样式
function  {
  return (
    <ThemeContext.Provider value="blue">
      <Toolbar/>
    </ThemeContext>
  );
}

Consumer 这个组件可以使你的函数式组件也能订阅广播的内容,就像上述案例一样,这种方法需要一个函数作为子元素。这个函数接收当前的 context 值,并返回一个 React 节点。传递给函数的 value 值等价于组件树上方离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue。

不过通过 useContex hook 可以更优雅的订阅广播的内容

import {ThemeContext} from '../context'
import {useContext} from 'react;

function ThemeButton () {
  const context = useContext(ThemeContext)
  
  return (
    <button style={{backgroundColor: value}}>ThemeButton</button>
  )
}

广播的其他内容

到此为止如果你已经掌握上述的内容,那么你就基本掌握广播的使用,下面将介绍一些扩展的内容

  1. Context.displayName

为了优化 开发工具,你可以给你的广播起一个名字这样你就可以在 react devtools 中轻松的找到它

案例

import React from 'react'

const ThemeContext = React.createContext('green')
ThemeContext.displayName = 'ThemeCont';
export ThemeContext
  1. 如果你要动态广播内容

如果你要动态广播内容,你就不能写死数据,你可以将数据缓存在根组件的state 中,然后再传给 Provider 的 value

  1. 在嵌套组件中更新 Context

此时你的广播必须是动态的,你可以在你的广播内容中添加一个方法,这个方法定义在根组件中用于修改根组件的state,此时你在广播state中的内容和这个方法,这样你的子组件就可以修改广播内容

  1. 消费多个Context

为了确保 context 快速进行重渲染,React 需要使每一个 consumers 组件的 context 在组件树中成为一个单独的节点。

// Theme context,默认的 theme 是 “light” 值
const ThemeContext = React.createContext('light');

// 用户登录 context
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    // 提供初始 context 值的 App 组件
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}

// 一个组件可能会消费多个 context
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

如果两个或者更多的 context 值经常被一起使用,那你可能要考虑一下另外创建你自己的渲染组件,以提供这些值。

注意

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

class App extends React.Component {
  render() {
    return (
      <MyContext.Provider value={{something: 'something'}}>
        <Toolbar />
      </MyContext.Provider>
    );
  }
}

优化上述情况

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }

  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值