前言
Context被翻譯為上下文,在編程領(lǐng)域,這是一個(gè)經(jīng)常會(huì)接觸到的概念,React中也有。
在React的官方文檔中,Context被歸類為高級(jí)部分(Advanced),屬于React的高級(jí)API,但官方并不建議在穩(wěn)定版的App中使用Contex。
不過(guò),這并非意味著我們不需要關(guān)注Context。事實(shí)上,很多優(yōu)秀的React組件都通過(guò)Context來(lái)完成自己的功能,比如react-redux的<Provider />,就是通過(guò)Context提供一個(gè)全局態(tài)的store,拖拽組件react-dnd,通過(guò)Context在組件中分發(fā)DOM的Drag和Drop事件,路由組件react-router通過(guò)Context管理路由狀態(tài)等等。在React組件開(kāi)發(fā)中,如果用好Context,可以讓你的組件變得強(qiáng)大,而且靈活。
初識(shí)React Context
當(dāng)你不想在組件樹(shù)中通過(guò)逐層傳遞props或者state的方式來(lái)傳遞數(shù)據(jù)時(shí),可以使用Context來(lái)實(shí)現(xiàn)跨層級(jí)的組件數(shù)據(jù)傳遞。
使用
props或者state傳遞數(shù)據(jù),數(shù)據(jù)自頂下流。使用Context,可以跨越組件進(jìn)行數(shù)據(jù)傳遞。
如何使用Context
如果要Context發(fā)揮作用,需要用到兩種組件,一個(gè)是Context生產(chǎn)者(Provider),通常是一個(gè)父節(jié)點(diǎn),另外是一個(gè)Context的消費(fèi)者(Consumer),通常是一個(gè)或者多個(gè)子節(jié)點(diǎn)。所以Context的使用基于生產(chǎn)者消費(fèi)者模式。
對(duì)于父組件,也就是Context生產(chǎn)者,需要通過(guò)一個(gè)靜態(tài)屬性childContextTypes聲明提供給子組件的Context對(duì)象的屬性,并實(shí)現(xiàn)一個(gè)實(shí)例getChildContext方法,返回一個(gè)代表Context的純對(duì)象 (plain object) 。
import React from 'react'
import PropTypes from 'prop-types'
class MiddleComponent extends React.Component {
render () {
return <ChildComponent />
}
}
class ParentComponent extends React.Component {
// 聲明Context對(duì)象屬性
static childContextTypes = {
propA: PropTypes.string,
methodA: PropTypes.func
}
// 返回Context對(duì)象,方法名是約定好的
getChildContext () {
return {
propA: 'propA',
methodA: () => 'methodA'
}
}
render () {
return <MiddleComponent />
}
}
而對(duì)于Context的消費(fèi)者,通過(guò)如下方式訪問(wèn)父組件提供的Context。
import React from 'react'
import PropTypes from 'prop-types'
class ChildComponent extends React.Component {
// 聲明需要使用的Context屬性
static contextTypes = {
propA: PropTypes.string
}
render () {
const {
propA,
methodA
} = this.context
console.log(`context.propA = ${propA}`) // context.propA = propA
console.log(`context.methodA = ${methodA}`) // context.methodA = undefined
return ...
}
}
子組件需要通過(guò)一個(gè)靜態(tài)屬性contextTypes聲明后,才能訪問(wèn)父組件Context對(duì)象的屬性,否則,即使屬性名沒(méi)寫(xiě)錯(cuò),拿到的對(duì)象也是undefined。
對(duì)于無(wú)狀態(tài)子組件(Stateless Component),可以通過(guò)如下方式訪問(wèn)父組件的Context
import React from 'react'
import PropTypes from 'prop-types'
const ChildComponent = (props, context) => {
const {
propA
} = context
console.log(`context.propA = ${propA}`) // context.propA = propA
return ...
}
ChildComponent.contextProps = {
propA: PropTypes.string
}
而在接下來(lái)的發(fā)行版本中,React對(duì)Context的API做了調(diào)整,更加明確了生產(chǎn)者消費(fèi)者模式的使用方式。
import React from 'react';
import ReactDOM from 'react-dom';
const ThemeContext = React.createContext({
background: 'red',
color: 'white'
});
通過(guò)靜態(tài)方法React.createContext()創(chuàng)建一個(gè)Context對(duì)象,這個(gè)Context對(duì)象包含兩個(gè)組件,<Provider />和<Consumer />。
class App extends React.Component {
render () {
return (
<ThemeContext.Provider value={{background: 'green', color: 'white'}}>
<Header />
</ThemeContext.Provider>
);
}
}
<Provider />的value相當(dāng)于現(xiàn)在的getChildContext()。
class Header extends React.Component {
render () {
return (
<Title>Hello React Context API</Title>
);
}
}
class Title extends React.Component {
render () {
return (
<ThemeContext.Consumer>
{context => (
<h1 style={{background: context.background, color: context.color}}>
{this.props.children}
</h1>
)}
</ThemeContext.Consumer>
);
}
}
<Consumer />的children必須是一個(gè)函數(shù),通過(guò)函數(shù)的參數(shù)獲取<Provider />提供的Context。
可見(jiàn),Context的新API更加貼近React的風(fēng)格。
總結(jié):
React的context就是一個(gè)全局變量,可以從根組件跨級(jí)別在React的組件中傳遞。React context 的API 有兩個(gè)版本,React16.x 之前 的是老版本的 context,之后的是新版本的context。
1.老版本的context
getChildContext根組件中聲明,一個(gè)函數(shù),返回一個(gè)對(duì)象,就是context childContextType根組件中聲明,指定context的結(jié)構(gòu)類 型,如不指定,會(huì)產(chǎn)生錯(cuò)誤
contextTypes子孫組件中聲明,指定要接收的context的結(jié)構(gòu)類型,可以只是context的一部分結(jié)構(gòu)。contextTypes 沒(méi)有定義,context將是一個(gè)空對(duì)象。
this.context 在子孫組件中通過(guò)此來(lái)獲取上下文
(注:從React v15.5開(kāi)始 ,React.PropTypes 助手函數(shù)已被棄用,可使用 prop-types 庫(kù) 來(lái)定義contextTypes)
舉例如下:
//根組件
class MessageList extends React.Component {
getChildContext() {
return {color: "purple",text: "item text"};
}
render() {
const children = this.props.messages.map((message) =>
<Message text={message.text} />
);
return <div>{children}</div>;
}
}
MessageList.childContextTypes = {
color: React.PropTypes.string
text: React.PropTypes.string
};
//中間組件
class Message extends React.Component {
render() {
return (
<div>
<MessageItem />
<Button>Delete</Button>
</div>
);
}
}
//孫組件(接收組件)
class MessageItem extends React.Component {
render() {
return (
<div>
{this.context.text}
</div>
);
}
}
MessageItem.contextTypes = {
text: React.PropTypes.string
};
class Button extends React.Component {
render() {
return (
<button style={{background: this.context.color}}>
{this.props.children}
</button>
);
}
}
Button.contextTypes = {
color: React.PropTypes.string
};
2.新版本的context
新版本的React context使用了Provider和Customer模式,和react-redux的模式非常像。在頂層的Provider中傳入value,
在子孫級(jí)的Consumer中獲取該值,并且能夠傳遞函數(shù),用來(lái)修改context,如下代碼所示:
//創(chuàng)建Context組件
const ThemeContext = React.createContext({
theme: 'dark',
toggle: () => {}, //向上下文設(shè)定一個(gè)回調(diào)方法
});
//運(yùn)行APP
class App extends React.Component {
constructor(props) {
super(props);
this.toggle = () => { //設(shè)定toggle方法,會(huì)作為context參數(shù)傳遞
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
};
this.state = {
theme: themes.light,
toggle: this.toggle,
};
}
render() {
return (
<ThemeContext.Provider value={this.state}> //state包含了toggle方法
<Content />
</ThemeContext.Provider>
);
}
}
//中間組件
function Content() {
return (
<div>
<Button />
</div>
);
}
//接收組件
function Button() {
return (
<ThemeContext.Consumer>
{({theme, toggle}) => (
<button
onClick={toggle} //調(diào)用回調(diào)
style={{backgroundColor: theme}}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}
案例2
// Context 可以讓我們無(wú)須明確地傳遍每一個(gè)組件,就能將值深入傳遞進(jìn)組件樹(shù)。
// 為當(dāng)前的 theme 創(chuàng)建一個(gè) context(“l(fā)ight”為默認(rèn)值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 使用一個(gè) Provider 來(lái)將當(dāng)前的 theme 傳遞給以下的組件樹(shù)。
// 無(wú)論多深,任何組件都能讀取這個(gè)值。
// 在這個(gè)例子中,我們將 “dark” 作為當(dāng)前的值傳遞下去。
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// 中間的組件再也不必指明往下傳遞 theme 了。
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// 指定 contextType 讀取當(dāng)前的 theme context。
// React 會(huì)往上找到最近的 theme Provider,然后使用它的值。
// 在這個(gè)例子中,當(dāng)前的 theme 值為 “dark”。
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
// 也可以按照這種形式獲取
function ThemedButton(){
return (
<ThemeContext.Counsumer>
{theme =>(
<Button theme={theme} />
)
}
</ThemeContext.Counsumer>
);
}
詳細(xì)用法可以參考官方文檔:https://react.docschina.org/docs/context.html#reactcreatecontext
3. context 在react hooks中的使用
React Hooks是React 16.8.6版本為函數(shù)式組件添加了在各生命周期中獲取state和props的通道??勺屇诓痪帉?xiě)類的情況下使用 state(狀態(tài)) 和其他 React 功能。不再需要寫(xiě)class組件,你的所有組件都將是Function。
hooks為我們提供了useContext方法來(lái)操作context。useContext可以很方便去訂閱context的改變,并在合適的時(shí)候重渲染組件。例如上面的函數(shù)式組件中,通過(guò)Consumer的形式獲取Context的數(shù)據(jù),有了useContext可以改寫(xiě)成下面:
function ThemedButton(){
const value = useContext(ThemeContxet);
return (
<Button theme={value} />
);
}
通過(guò)useCOntext我們可以直接拿到context的值,而不再需要在函數(shù)式組件外面用context.Consumer包裹了
4. context在如下的生命周期鉤子中可以使用
constructor(props, context)
componentWillReceiveProps(nextProps, nextContext)
shouldComponentUpdate(nextProps, nextState, nextContext)
componentWillUpdate(nextProps, nextState, nextContext)
componentDidUpdate(prevProps, prevState, prevContext)
5. 在無(wú)狀態(tài)組件中可以通過(guò)參數(shù)傳入
function D(props, context) {
return (
<div>{this.context.user.name}</div>
);
}
D.contextTypes = {
user: React.PropTypes.object.isRequired
}
5. React context的局限性
- 在組件樹(shù)中,如果中間某一個(gè)組件
ShouldComponentUpdate returning false了,會(huì)阻礙 context 的正常傳值,導(dǎo)致子組件無(wú)法獲取更新。 - 組件本身
extends React.PureComponent也會(huì)阻礙 context 的更新。
注意點(diǎn):
- Context 應(yīng)該是唯一不可變的.
- 組件只在初始化的時(shí)候去獲取 Context
參考: