丟擲問題
在平時使用react的過程中,資料都是自頂而下的傳遞方式,例如,如果在頂層元件的state儲存了theme主題相關的資料作為整個App的主題管理。那麼在不借助任何第三方的狀態管理框架的情況下,想要在子元件裡獲取theme資料,就必須的一層層傳遞下去,即使兩者之間的元件根本不需要該資料;就如同下圖所示,並且如果App的層級越深,這之間的層層傳遞對開發者來說可謂是災難。
引入context
正因為有了跨越層級傳遞值的這麼一種需求,其實官方也提供了context的機制。通過context,我們就能夠在子元件裡獲取祖先元件裡的值,而不需要層層傳遞。其實很多的狀態管理框架與react結合的庫就是使用了context特性,例如著名的react-redux。
在v16.3之前的context只是官方的實驗性API,其實官方是不推薦開發者使用的,但是架不住很多框架依舊在使用它;所以官方在v16.3釋出的新的context API,新的API會更加的易用,本文也是以v16.3為準。
在新的context API中,React提供了一個createContext的方法,該方法返回一個包含了Provider,Consumer的物件,而Provoider,Consumer物件就是新API的重點。
我們先看一個簡單的例子,再來講解API。在本案例中,在頂層父元件的state儲存著控制這個App的theme的一些屬性,使用context來跨元件傳遞這些屬性,使得底層元件能夠直接得到這些屬性。
首先在themeContext.js檔案中定義context,並匯出Provider以及Consumer:
import {createContext} from "react";
export const {Provider, Consumer} = createContext({
color: "green",
fontSize: "20px"
});
複製程式碼
createContext需要傳遞一個引數,叫做defaultValue。這個值會在什麼時候起作用呢?這個稍後解釋。
然後我們就可以直接在頂層的App元件中,直接使用Provider:
import React, {Component} from 'react';
import {Provider} from "./context/themeContext";
import Parent from "./Parent";
class App extends Component {
state = {
color: "red",
fontSize: "16px"
};
render() {
return (
<div className="App">
<Provider value={this.state}>
<Parent/>
</Provider>
</div>
);
}
}
export default App;
複製程式碼
我們直接在頂層的元件裡使用Provider元件,並且Provider元件有一個value屬性用於傳遞context的實際的value。然後我們就可以在底層的Child元件中得到這些value來使用。
層級關係:App -> Parent -> Child
import React, {PureComponent} from "react";
import {Consumer} from "./context/themeContext";
class Child extends PureComponent {
render() {
return <Consumer>
{
style => <div style={style}>This is Child Component that gets style value through context.</div>
}
</Consumer>
}
}
export default Child;
複製程式碼
在Child元件中使用Consumer,就能夠得到上層所傳遞context的值;Consumer的需要一個函式作為子元素,該函式的引數就是上層所傳遞context value,然後就可以返回該元件具體的元件樣式了。
這就是一個簡單的使用context的例子,可以看到context的API是非常簡單的,也可容易使用,再簡單總結一下API:
- createContext:用於建立context,需要一個defaultValue的引數,並返回一個包含Provider,以及Consumer的物件
- Provider:頂層用於提供context的元件,包含一個value的props,value是實際的context資料
- Consumer:底層用於獲取context的元件,需要一個函式作為其子元素,該函式包含一個value的引數,該函式的引數就是上層所傳遞context value
看到這裡,你可能會有一個疑惑:為什麼createContext需要一個defaultValue,而Provider還需要一個實際的value?到底defaultValue是什麼時候起作用呢?先丟擲結論:只有在上層元件沒有提供Provider元件時,下層元件的Consumer才會直接使用defaultValue作為子函式的引數傳遞。以本例子為例,只有在App元件壓根沒有使用Provider元件時,Child元件中的Consumer的子函式引數才會是{ color: "red", fontSize: "16px" }
這個defaultValue,其他情況都不會使用到這個值。這個地方有一個常見的誤解:就是不給上層元件的Provider的value屬性,或者讓value={undefined}
時,就會使用defaultValue,這是不對的!!!請切記,大家也可以自己嘗試,看看是不是這個結論。
更近一步
雖然使用了Consumer能夠讓我們很方便的得到context的value,但是如果很多子元素要得到context的值,都去先呼叫Consumer,再在它的子函式裡返回真正的元件內容,會顯得十分的累贅。所以我們可以對Consumer進行一個簡單的封裝,封裝一個connect的方法。去實現類似於react-redux其中的connect函式的效果。connect方法的程式碼如下:
import React from "react";
import {Consumer} from "./context";
export default mapState => {
return WrappedComponent => {
const Component = props => (<Consumer>
{
value => {
let mappedProps = mapState(value);
return <WrappedComponent {...props} {...mappedProps}/>
}
}
</Consumer>);
Component.displayName = `connect(${WrappedComponent.displayName || WrappedComponent.name || "Component"})`;
return Component;
}
};
複製程式碼
簡單解釋一下:connect方法需要傳入一個mapState方法,mapState方法是context的value對映方法,當呼叫connect方法後,會依舊返回一個函式;該函式實際是一個高階函式工廠,將傳入的WrappedComponent元件用Consumer包裹裡面,並結合之前的mapState對映得到具體的計算後的props屬性,並把這些props屬性都賦予給WrappedComponent。這樣,我們在之後想要得到context時,只需要簡單呼叫一下該方法即可。
再結合一個例子看看怎麼使用connect方法:假如現在有一個App使用者顯示學生的相關資訊;學生的資訊包含了name,age,gender三個屬性;此外有兩個元件Student、StudentGender;Student用於顯示學生的name,age,並且有一個+
按鈕,點選就會在當前年齡加一歲。
層級關係如下:App -> StudentContainer -> Student
App元件的程式碼如下:
import React, {Component} from 'react';
import {Provider} from "./context";
import StudentContainer from "./StudentContainer";
class App extends Component {
onIncreaseAge = () => {
this.setState(preState => ({
age: preState.age + 1
}))
};
state = {
name: "張三",
age: 12,
gender: "男",
onIncreaseAge: this.onIncreaseAge
};
render() {
return (
<div className="App">
<Provider value={this.state}>
<StudentContainer/>
</Provider>
</div>
);
}
}
export default App;
複製程式碼
在App元件中,我們將student的屬性以及增加年齡的方法一同傳遞給了context,使得子元件既能獲得屬性,也能呼叫修改屬性的方法。
Student元件的程式碼如下:
import React from "react";
import {connect} from "./context";
const Student = ({studentName, studentAge, onIncreaseAge}) => {
return <div>
<span className="title">Student:</span>
<ul>
<li>name: {studentName}</li>
<li>age: {studentAge}
<button onClick={onIncreaseAge}>+</button>
</li>
</ul>
</div>;
};
const mapState = state => ({
studentName: state.name,
studentAge: state.age,
onIncreaseAge: state.onIncreaseAge
});
export default connect(mapState)(Student);
複製程式碼
可以看到,當我們使用了connect方法後,Student元件就變成了一個傻瓜元件,只需要專心負責顯示資料即可。
結語
以上就是關於context的簡單介紹,可以看到它確實十分簡單的實現了跨層級傳遞資料的功能。所以當我們想要跨層級傳遞資料時,而資料本身要傳遞的地方不多,這個時候往往不想再引入一個更復雜的狀態管理框架(如redux等),這個時候,context會是一個十分不錯的選擇。
本文所涉及到的案例的地址在此,其中第一個案例在分支sample-theme
中,第二個案例在分支encapsulate
中。
如果對本文有什麼意見和建議,歡迎討論和指正!!!