【一文一问】两种方式实现React Context

949 阅读2分钟

一,应用背景

React是单向数据流。父组件通过props传值给子组件,子组件通过props传值给孙组件,数据从顶层一层层往下流。而state,是组件的私有状态,一般用于组件内部的数据更改。

假设有个这样的需求:有三个组件father、son1、son2,son2需要用到son1的某个状态。可以通过函数的方式,将son1的state暴露给父组件,再由父组件通过props传值给son2。可是,如果这个son2在很深很深的层级呢,难道要一级一级地通过props传值吗?单项数据流的好处是可追溯性,利于定位bug,但同时也带来了不便。context就此应运而生。

context可以实现组件间的数据共享,而不必层层传递数据。

二,通过provider、consumer实现context

1,React.createContext()

语法:

const MyContext = React.createContext(defaultValue);

示例:

import React from 'react'
console.log('React', React)
const ThemeContext = React.createContext('pink')
console.log('ThemeContext',ThemeContext )

image.png

image.png createContext()是挂在React上的一个函数,调用这个函数,并传入默认值,会返回一个Context对象。Context对象上有两个重要的属性:Provider和Consumer,稍后详细说明。

2,Context.Provider、Context.Consumer

语法:

<MyContext.Provider value={/* 某个值 */}>  //供给者
<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}  //消费者
</MyContext.Consumer>

示例:

结构:

image.png

// App.js 父组件
import React from 'react'
const Content = React.lazy(()=>import('./content.js'))

const ThemeContext = React.createContext('pink') //创建context对象
console.log('ThemeContext',ThemeContext )
export const {Provider, Consumer} = ThemeContext //从context对象中结构出Provider&Consumer,并export

function App() {
  return (
    <div className="App">
      <Provider value='skyblue'>  //为子孙组件提供公共数据
          <Content/>
      </Provider>
    </div>
  );

//content.js 子组件
import Button from './button'
const Content = ()=>{
  return (
    <>
     <h2>hello context</h2>
     <Button/>
    </>
  )
}
export default Content
//button.js 孙组件
import {Consumer} from './App'  //从父组件中引入consumer
function Button() {
  return (
    <Consumer>
      {
        (value)=><button style={{backgroundColor:`${value}`}}>click</button>
      }
    </Consumer>
  )
}

效果:

image.png

注意一为什么最后起作用的是<Provider value='skyblue'> ,而不是defaultValue

官方解释:当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

也就是说,因为provider提供了值,所以就取provider中的值,只有provider不存在时,才会用 defaultValue。注意,provider的value必传,不传会报错,value设置为空或者undefined时,既不会使用defaultValue,孙组件也取不到来自父组件的数据

//App.js 父组件
const ThemeContext = React.createContext('pink')
export const {Provider, Consumer} = ThemeContext

function App() {
  return (
    <div className="App">
      {/* <Provider value={undefined}> */} //注掉Prvider
          <Content/>
      {/* </Provider> */}
    </div>
  );
}
// button.js 孙组件
import {Consumer} from './App'
function Button() {
  return (
    <Consumer>
      {
        (value)=><button style={{backgroundColor:`${value}`}}>click</button>
      }
    </Consumer>
  )
}

此时,用了defaultValue。效果:

image.png

注意二 Context.Consumer需要一个函数作为子元素

这个函数接收当前的 context 值,并返回一个 React 节点。传递给函数的 value 值等价于组件树上方离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue

函数式组件和类组件中都可用Context.Consumer

//button.js 孙组件
import {Consumer} from './App'

class Button extends React.Component{
  render(){
    return (
      <Consumer>
        {
         (value)=><button style={{backgroundColor:`${value}`}}>click</button>
        }
      </Consumer>
    )
  }
}

三,通过Class.contextType实现Context

示例,结构同二:

// App.js 父组件
import React from 'react'

const Content = React.lazy(()=>import('./content.js'))

export const ThemeContext = React.createContext('pink') //同样地,也要调用React.createContext()。同时将返回的context对象导出

function App() {
  return (
    <div className="App">
       <Content/>
    </div>
  );
}

export default App;

//content.js 子组件
import Button from './button'
const Content = ()=>{
  return (
    <>
     <h2>hello context</h2>
     <Button/>
    </>
  )
}
export default Content

方式一,Class.contextType = 引入的context对象

// button.js 孙组件
import React from 'react'
import {ThemeContext} from './App' //从父组件中引入ontext对象

class Button extends React.Component{
  render(){
    let value  = this.context //从button类的context属性中拿到传来的数据,赋值给value
    return (
     <button style={{backgroundColor:`${value}`}}>click</button>
    )
  }
}

Button.contextType = ThemeContext //将button类的contextType属性赋值为引入的context对象

export default Button

方式二,实验性的 public class fields 语法

语法:

class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* 基于这个值进行渲染工作 */
  }
}

示例:

import React from 'react'
import {ThemeContext} from './App' //引入context对象

class Button extends React.Component{
  static contextType = ThemeContext //将引入的context对象赋值给 contextType
  render(){
    let value  = this.context
    return (
     <button style={{backgroundColor:`${value}`}}>click</button>
    )
  }
}

效果:

image.png 与provider相比,contextType一开始就用了defaultValue。

注意只有类组件才能用contextType哦,provider+consumer函数式组件和类组件都可以用。

四,参考文献

1,Context – React (docschina.org)

2,React Context(上下文) 作用和使用 看完不懂 你打我 - 简书 (jianshu.com)