React狀態管理之Context

AHOhhhh發表於2019-02-25

丟擲問題

在平時使用react的過程中,資料都是自頂而下的傳遞方式,例如,如果在頂層元件的state儲存了theme主題相關的資料作為整個App的主題管理。那麼在不借助任何第三方的狀態管理框架的情況下,想要在子元件裡獲取theme資料,就必須的一層層傳遞下去,即使兩者之間的元件根本不需要該資料;就如同下圖所示,並且如果App的層級越深,這之間的層層傳遞對開發者來說可謂是災難。

react資料流.png

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

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

相關文章