回顧 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 歸納成以下幾個步驟:
創建 React context object
在 Provider 中放入值,以將該值廣播給自己以下的 Consumer component 使用
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 查看內容。
Provider
與 Consumer
的詳細內容將在下面介紹。
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.Provider
的 value
改變,則所有使用此 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.Provider
的 value
改變與否則是使用 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
會分為以下兩個步驟:
設定要使用的 context object
使用 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. componentDidMount
、componentDidUpdate
...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.contextType
與 Context.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 的範例以及特殊使用技巧。
https://reactjs.org/docs/context.html
https://blog.logrocket.com/pitfalls-of-overusing-react-context/
https://leewarrick.com/blog/the-problem-with-context/
https://kentcdodds.com/blog/application-state-management-with-react
https://kentcdodds.com/blog/prop-drilling
https://stackoverflow.com/a/62464737/7855714
https://stackoverflow.com/a/54283511/7855714