React中,数据流是自顶向下的,如果兄弟组件通信,那就得先状态提升到父组件
但我们平时开发过程中,经常碰到组件树层级很深,如果不同层级的组件需要使用同一份数据,那从顶层组件分别传递props的方案肯定是很麻烦的
而且太深的props层级对后续进行维护追溯数据源来说也是不好的一种解决方式
因此context的使用场景就是:在组件树中,不同层级需要访问相同的数据源时,可以利用context,进行组件之间的通信
React.context
先看这么一个例子:
// context.js
import React from 'react'
class Bottom extends React.Component {
render() {
return (
<div>
<h3>Data in Bottom: { this.props.data }</h3>
</div>
)
}
}
class Middle extends React.Component {
render() {
return (
<div>
<h3>Data in Middle: { this.props.data }</h3>
<Bottom {...this.props}></Bottom>
</div>
)
}
}
export default class Top extends React.Component {
render() {
return (
<div>
<h3>Data in Top: { this.props.data }</h3>
<Middle {...this.props}></Middle>
</div>
)
}
}
// app.js
class App extends React.Component {
render() {
return (
<Top data="source data"></Top>
)
}
}
上面这个例子中,我们底层的Bottom组件如果想使用Top组件中的data属性,得通过Top -> Middle -> Bottom逐层将data传递下来
显而易见,很不方便
这个时候我们就可以使用context:
新建一份文件TopContext.js,创建一个context对象,用于存放不同子组件需要共同使用的数据源:
// TopContext.js
import React from 'react'
export const TopContext = React.createContext({
data: "default data"
})
注意,createContext中的参数并不是Provider的初始value,而是当没有提供Provider的时候,各个组件消费时的初始值。
然后,引入Top组件的时候用Provider包裹,通过value属性,将需要共享的数据源的值传入:
// app.js
import { TopContext } from './TopContext.js'
class App extends React.Component {
render() {
return (
<TopContext.Provider value={{ data: "source data" }}>
<Top></Top>
</TopContext.Provider>
)
}
}
然后在子组件中,我们就可以进行接收Provider提供的数据了,接收的方式有两种
1、利用contextType:
// context.js
import { TopContext } from './TopContext'
class Bottom extends React.Component {
render() {
console.log("Bottom context: ", this.context)
return (
<div>
<h3>Data in Bottom: { this.context.data }</h3>
</div>
)
}
}
Bottom.contextType = TopContext
该属性用一个createContext构造的context对象赋值,赋值之后,组件内部的this.context属性就可以消费该context上的数据了
但是这个方式有个问题:只能订阅单一的context,因为contextType只能赋值一个context对象
所以我们可以用第二种方式
2、利用Consumer:
// context.js
class Middle extends React.Component {
render() {
return (
<TopContext.Consumer>
{
value => {
console.log("value:", value)
return (
<div>
<h3>Data in Middle: { value.data }</h3>
<Bottom></Bottom>
</div>
)
}
}
</TopContext.Consumer>
)
}
}
利用对应context的Consumer,来消费对应context的数据
利用Consumer的内部的value,拿到context中的数据源,然后提供给我们的组件使用
如果这个时候有多个context,实际上就是多套几层函数:
// TopContext.js
export const TopContext = React.createContext({
data: "default data"
})
export const TestContext = React.createContext({
test: "test"
})
Provider嵌套:
// app.js
import { TopContext, TestContext } from './TopContext.js'
class App extends React.Component {
render() {
return (
<TopContext.Provider value={{ data: "source data" }}>
<TestContext.Provider value={{ test: "test data" }}>
<Top></Top>
</TestContext.Provider>
</TopContext.Provider>
)
}
}
Consumer嵌套:
import { TopContext, TestContext } from './TopContext'
class Middle extends React.Component {
render() {
return (
<TopContext.Consumer>
{
topValue => (
<TestContext.Consumer>
{
testValue => {
console.log("topValue:", topValue)
console.log("testValue:", testValue)
return (
<div>
<h3>Data in Middle: { topValue.data }</h3>
<Bottom></Bottom>
</div>
)
}
}
</TestContext.Consumer>
)
}
</TopContext.Consumer>
)
}
}
这样就可以实现组件的跨层级订阅数据源了
当然跨层级的场景不单单只涉及到获取,肯定有时候也需要对数据源进行修改。
其实也挺简单,就是把修改函数也作为数据源的一部分传进去:
TopContext.js中创建默认函数:
// TopContext.js
import React from 'react'
export const TopContext = React.createContext({
data: "default data",
changeData: () => {}
})
app.js中定义该函数:
// app.js
import { TopContext, TestContext } from './TopContext.js'
class App extends React.Component {
state = {
data: "source data",
changeData: () => {
this.setState({
data: this.state.data + "haha~"
})
}
}
render() {
return (
<TopContext.Provider value={ this.state }>
<TestContext.Provider value={{ test: "test data" }}>
<Top></Top>
</TestContext.Provider>
</TopContext.Provider>
)
}
}
上面的例子中,我们将Provider的数据data以及函数changeData都放到了state对象中
在子组件中,调用该更新函数即可:
// context.js
class Bottom extends React.Component {
render() {
console.log("Bottom context: ", this.context)
return (
<div>
<h3>Data in Bottom: { this.context.data }</h3>
<button onClick={this.context.changeData}>Bottom Change Data</button>
</div>
)
}
}
Bottom.contextType = TopContext
Consumer的方式同理,更新函数可以在回调参数中拿到,子组件使用即可
一些注意事项:
给Provider的value属性赋值为对象的时候,不要将该对象直接写在render函数内部,将其放入组件的state中。不然每次父组件render的时候,因为value为重建一个对象会导致子组件也重新渲染
上面的例子也可以看到,才用了连个context,Consumer的嵌套已经不浅了,所以我们使用context的时候尽量遵循一个单一数据源的原则,否则很容易造成嵌套地狱(wrapper hell)
useContext
通过上面的文章,我们其实可以看到,当我们以Consumer的方式对Provider的数据进行使用时,函数式组件和类组件其实差别不大
我们将middle组件改一下,也能正常使用:
// context.js
const Middle = () => {
return (
<TestContext.Consumer>
{
value => (
<div>
<h3>Data in Middle: {value.data}</h3>
<Bottom></Bottom>
</div>
)
}
</TestContext.Consumer>
)
}
但是很显然contextType这种方式对于FC来说就不行了
既然有了useContext这个hook,那在函数式组件中,我们就以useContext的方式来:
// context.js
import React, { useContext } from 'react'
const Middle = () => {
const value = useContext(TopContext)
return (
<div>
<h3>Data in Middle: {value.data}</h3>
<Bottom></Bottom>
</div>
)
}
useContext实际上相当于Context.Consumer 或者 contextType = MyContext 的作用,用来订阅指定的context对象
网友评论