iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 27
1
Modern Web

I Want To Know React系列 第 27

I Want To Know React - Context 語法

回顧 Context

上一章節中,我們介紹了何謂 context。

Context 是一種利用向下廣播來傳遞資料的方式,此方法可以解決 props 必須要一層層向下傳遞的缺點。

而根據 React 官方推薦,我們應該只在要將全域性資料(e.g. 使用者資訊、時區設定、語系、UI 主題 ...etc)向下傳遞給很多底層 component 時才應該使用 context。

如果是要避免中間層 component 傳遞過多細節 props 的話,開發者可以使用 composition 的技巧,把整個下層 component 作為 props 傳遞下去即可。

在本章節中,我們將介紹 context 的詳細語法。

Context 使用概念

在開始前,先讓我們來概述一下使用 context 的概念。

Context 角色

React context 的使用會環繞三個角色在運作:

  • Context Object
  • Provider
  • Consumer

一個 React app 中可以有多個 React context。每個 React context 的本體都是一個物件(在這邊把它稱為 context object)。其中 context object 中又會有兩個很重要的屬性:Provider(提供者)與 Consumer(消費者)。

  • Provider(提供者)的功用就是用來提供 context 值。
  • Consumer(消費者)的功用則是用來使用 context 值。

使用 Provider 的 component 與使用 Consumer 的 component 之間不需要是直接的父子層關係。Provider 只要在 Consumer 的上層即可讓 Consumer 接收到 context 值,而處於 Provider 與 Consumer 之間的中間層 component 則不須做任何的改動。

Context 使用步驟

根據以上的資訊,我們可以把使用 Context 歸納成以下幾個步驟:

  1. 創建 React context object
  2. 在 Provider 中放入值,以將該值廣播給自己以下的 Consumer component 使用
  3. Consumer 接收值,可根據接收到的值 component 可顯示對應的內容或執行對應的動作

接下來,就讓我們進入介紹語法的篇章吧!

React.createContext

首先要先學習如何創建一個 React context。

創建 React context 需使用 React.createContext 這個函式。

更詳細一點說明,這個函式有兩個功能:

  • 創建 context object(或說 context type)
  • 設定 context 的預設值

語法如下所示:

const MyContext = React.createContext(defaultValue);

React.createContext 需要帶入一個參數:

  • defaultValue:代表這個 context 的預設值,與 props 一樣,可為任意的值

    因為代表預設值,所以只有在 Consumer 以上的 component 中都沒有 Provider 時才會使用到 defaultValue 的內容。

    需要注意的是,如果 Consumer 上面有 Provider,但此 Provider 的值為 undefined 的話,則 Consumer 依然不會使用 defaultValue,拿到的值會是 undefined

React.createContext 會回傳一個值:

  • Context object:也可以說是 context type,會是一個 JavaScript object

    在下一個段落中會更詳細的介紹 context object。

Context Object

接著要介紹 Context object。

如剛剛所講,Context object(Context type)會是一個 JavaScript object。每個 Context object 中會有兩個很重要的屬性:

  • Provider
  • Consumer

把 context object 的 log 下來的話,即可看到這兩個屬性:

console.log(MyContext);
// {$$typeof: Symbol(react.context), Consumer: {$$typeof: Symbol(react.context), _context: {…}, …}, Provider: {$$typeof: Symbol(react.provider), _context: {…}}, _calculateChangedBits: null, _currentValue: 123, _currentValue2: 123, _threadCount: 0, …}

讀者也可以到 CodePen 上的 console 查看內容。

ProviderConsumer 的詳細內容將在下面介紹。

Context.Provider

每個 Context 物件中都會有 Provider 屬性,其用途就是將指定的值傳給更下層的 Consumer 使用。

Context.Provider 是一個 React element,因此可以使用 JSX 表示。語法如下:

<MyContext.Provider value={/* some value */}>{/* ... */}</MyContext.Provider>

Context.Provider 可以帶入一個 prop:

  • value:代表提供給 Provider 以下的 Consumer 使用的值,與 props 一樣,可為任意的值。

    換句話說,Provider 提供了 value 後,更下層的 component 都可以用 Context.Consumer 接收 value 的內容。

可以巢狀使用 Context.Provider

Context.Provider 中可以再包裹 Context.Provider

當巢狀使用同一個 Provider 時,內層的 Provider 會遮蔽掉(覆蓋,而非 Merge)外層 Provider 的 value

也就是說內層Provider 以下的 component 會拿到內層的 value;介於內外層 Provider 之間的 component 則還是會拿到外層 Provider 的 value

讀者可以查看此 CodePen 範例。

Context.Provider 的 value 改變時,底下的 consumer 必定會 re-render

需要注意的是,一旦 Context.Providervalue 改變,則所有使用此 Provider 以下的 Customer component 都會 re-render。

此 re-render 不會因為 shouldComponentUpdate 為 false 而取消,也不會因為 Consumer 以上的中間層的 component 沒有更新而不執行。也就是這些 Consumer component "必定" 會 re-render。

因此在使用 Context.Provider 時需要注意不要 inline 賦值給 value,否則當使用 Provider 的 component re-render 時,使用此 Provider 的 Consumer 都會一併 re-render。這將造成極大的效能問題。

Value 改變與否是用 Object.is 來判斷

至於 Context.Providervalue 改變與否則是使用 Object.is 來判斷。

Object.is 基本上就是 === 的概念,只是額外增加了 NaN±0 的判斷而已。

Object.is 的詳情資訊請參考 MDN 的介紹

Context.Consumer

每個 Context 物件中都會有 Consumer 屬性,其用途就是接收上層 Provider 傳下來的值。

Context.Consumer 是一個 React element,因此可以使用 JSX 表示。語法如下:

<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>
// {$$typeof: Symbol(react.element), type: {…}, key: null, ref: null, props: {…}, …}

Context.Consumer 可以帶入一個 prop:

  • children:代表要 render 的內容,會是一個 function

    需要注意的是,children 是一個 function 而非 React element,如果 children 不是帶入 function 的話,React 就會報警告。

    使用 function 當作 children 是 Render props 的技巧,此技巧將在之後章節介紹。

    children function 接受一個參數:

    • value:代表 Consumer 接收到的值,與 props 一樣,可能為任意的值。

    children function 會回傳一個值:

    • React element:即代表要 render 出來顯示在畫面上的內容

Consumer 會接收最靠近自己的 Provider 的值

Consumer 接收到的值會是由最靠近自己的 Provider 所提供的。

也就是,如果有巢狀的 Provider 時,Consumer 拿到的值會是靠最內層 Provider 所提供的,較外層 Provider 的值會被遮蔽掉。

舉例來說:

const MyContext = React.createContext({ theme: "default" });

const consumer = (
  <MyContext.Provider value="outerProvider">
    <MyContext.Provider value="innerProvider">
      <MyContext.Consumer>{(value) => <div>{value}</div>}</MyContext.Consumer>
    </MyContext.Provider>
  </MyContext.Provider>
);

ReactDOM.render(consumer, document.getElementById("root"));

範例中,有兩個 Provider 包裹一個 Consumer。Consumer 拿到的會是最靠近自己的 Provider 所提供的值 "innerProvider",而非外層 Provider 提供的 "outerProvider"

如果讀者想要自己試試的話可以參考這個 CodePen 範例。

Class.contextType

React class component 可以接受名為 contextType 的屬性。此屬性的用處與 Context.Consumer 相同,都是用來接收上層 Provider 傳下來的值。

但與 Context.Consumer 不同,使用 Class.contextType 會分為以下兩個步驟:

  1. 設定要使用的 context object
  2. 使用 context 內容

設定 contextType 語法

因為 class 本身並不會知道開發者要使用的 context 為何,因此要先指定 context object。

設定要使用的 context 的語法如下:

class MyClass extends React.Component {
  // ...
}
MyClass.contextType = MyContext;

如果專案有支援實驗性 public class fields syntax 語法的話,也可使用 static 設定 contextType,兩種語法效果相同:

class MyClass extends React.Component {
  static contextType = MyContext;
  // ...
}

因為是設定 context,因此 contextType 只能接受以下型別:

  • Context object:即代表要使用的 context

    如果 contextType 的內容不是 context object 的話,則 React 會報警告。

    MyClass.contextType = 123;
    // Warning: TestContextType defines an invalid contextType. contextType should point to the Context object returned by React.createContext().
    

取用 context 內容語法

接著來到取用 context 內容的部分。

如果要使用 contextType 方式取用 context 內容的話,則需要在 class 內使用 this.context

class MyClass extends React.Component {
  // ...
  render() {
    let value = this.context;
    /* render something based on the value of MyContext */
  }
}
MyClass.contextType = MyContext;

this.context 的內容與 Context.Consumer 接收到的值相同,都代表 Provider 傳下來的 value

另外,this.context 的特性與 Context.Consumer 相同,都是接收最靠近自己的 Provider 的值。

this.context 可以在 lifecycle 函式中取用

另外一點與 Context.Consumer 不同的地方是,this.context 可以在所有 lifecycle 函式中取用,而不只限制在 render 函式中使用而已。

也就是說,commit 階段的 lifecycle 函式(e.g. componentDidMountcomponentDidUpdate ...etc)就可以取用 this.context 的值來執行各種 side-effect:

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* perform a side-effect at mount using the value of MyContext */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* render something based on the value of MyContext */
  }
}
MyClass.contextType = MyContext;

Context.Consumer vs. Class.contextType

如上面段落所提及,Class.contextTypeContext.Consumer 功能相同,都是用來取用最靠近自己的 context 值,然而它們之間還是有些區別,如下所示:

從可以使用的 component 種類來看

  • Context.Consumer 可以在 class component 與 function component 中使用
  • Class.contextType 則只能在 class component 中使用

從可以取用 context 的位置來看

  • Context.Consumer 因為是 React element,所以只能在 render 函式中使用
  • Class.contextType 則可以在各種 lifecycle 函式中使用,因此可以支援取用 context 值執行 side-effect

從可以使用的 context 數量來看

  • Context.Consumer 外還可以再包其他的 Consumer,因此一個 component 可以使用多種 context object
  • Class.contextType 因為語法的限制,一個 class component 只能指定使用一種 context object

Context.displayName

最後,context object 可以支援使用 displayName 屬性,可讓 React dev tool 上的 context 顯示為 displayName 設定的名稱:

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider /> // "MyDisplayName.Provider" in React DevTools
<MyContext.Consumer /> // "MyDisplayName.Consumer" in React DevTools

因為是設定名稱,displayName 只能接受以下型別:

  • String:代表 React dev tool 上 context 的名稱

預設的 context displayName 就是 "Context"

如果不設定 displayName 的話,context 預設的 displayName 就是 "Context"。

在有多個 context object 的狀況下,如果都沒設定 displayName 就會很難在 React dev tool 上區別各個 context,如下所示:

const MyContext = React.createContext(/* some value */);
const MyOtherContext = React.createContext(/* some value */);

<MyContext.Provider /> // "Context.Provider" in React DevTools
<MyContext.Consumer /> // "Context.Consumer" in React DevTools
<MyOtherContext.Provider /> // "Context.Provider" in React DevTools
<MyOtherContext.Consumer /> // "Context.Consumer" in React DevTools

因此建議讀者在使用 context 時都盡量設定 displayName

小結

本章節介紹了 React context 的用法。

React context 的使用會環繞三個角色在運作:

  • Context object:代表 context 本身
  • Provider:用來提供 context 值
  • Consumer:用來使用 context 值

使用 Provider 的 component 與使用 Consumer 的 component 之間不需要是直接的父子層關係,只要 Provider 在 Consumer 的上層即可讓 Consumer 接收到 context 值。

另外,我們也介紹了以下幾個 context 語法:

  • React.creatContext
  • Context.Provider
  • Context.Consumer
  • Class.contextType
  • Context.displayName

在下一章中,我們將介紹一些 context 的範例以及特殊使用技巧。

參考資料


上一篇
I Want To Know React - 初探 Context
下一篇
I Want To Know React - Context 範例 & 使用技巧
系列文
I Want To Know React30

尚未有邦友留言

立即登入留言