React狀態管理之Context

拋出問題

在平時使用react的過程當中,數據都是自頂而下的傳遞方式,例如,若是在頂層組件的state存儲了theme主題相關的數據做爲整個App的主題管理。那麼在不借助任何第三方的狀態管理框架的狀況下,想要在子組件裏獲取theme數據,就必須的一層層傳遞下去,即便二者之間的組件根本不須要該數據;就如同下圖所示,而且若是App的層級越深,這之間的層層傳遞對開發者來講可謂是災難。 react

react數據流.png

引入context

正由於有了跨越層級傳遞值的這麼一種需求,其實官方也提供了context的機制。經過context,咱們就可以在子組件裏獲取祖先組件裏的值,而不須要層層傳遞。其實不少的狀態管理框架與react結合的庫就是使用了context特性,例如著名的react-redux。git

在v16.3以前的context只是官方的實驗性API,其實官方是不推薦開發者使用的,可是架不住不少框架依舊在使用它;因此官方在v16.3發佈的新的context API,新的API會更加的易用,本文也是以v16.3爲準。github

在新的context API中,React提供了一個createContext的方法,該方法返回一個包含了Provider,Consumer的對象,而Provoider,Consumer對象就是新API的重點。redux

咱們先看一個簡單的例子,再來說解API。在本案例中,在頂層父組件的state存儲着控制這個App的theme的一些屬性,使用context來跨組件傳遞這些屬性,使得底層組件可以直接獲得這些屬性。api

首先在themeContext.js文件中定義context,並導出Provider以及Consumer:app

import {createContext} from "react";

export const {Provider, Consumer} = createContext({
  color: "green",
  fontSize: "20px"
});
複製代碼

createContext須要傳遞一個參數,叫作defaultValue。這個值會在何時起做用呢?這個稍後解釋。框架

而後咱們就能夠直接在頂層的App組件中,直接使用Provider:ide

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 -> Childthis

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中。

若是對本文有什麼意見和建議,歡迎討論和指正!!!

相關文章
相關標籤/搜索