React是單向資料流,利用 React.createElement 建構出整個 element tree 結構,使用者利用 state 及 props 處理元件資料,並搭配觸發 react 更新元件。因為 props 是需要傳遞的,所以時常會遇到 props 需要傳很多層。React為了解決這問題,建立了 context API 的功能,要功能就是跨元件傳遞資料,像是知名的 state 管理工具 react-redux 就是依賴 context 實現的。

最近就遇到所有 API Error handle 都需要用到新欄位的值,這個值就幾乎傳到到所有元件,中間還不小心遺漏傳遞一個元件,發生些問題…。現在回過頭想想用 context 處理問題會少很多。

React Context

  • Pass props Hell
    <Header islogin={islogin}/>
    // inside Header Element
    <Navbar islogin={islogin} />
    // inside Navbar Element
    <Account islogin={islogin} />
    // inside Account Element
    <User islogin={islogin} />
    {islogin ?
    <Button onClick={Logout}>Logout</Button>
    :

    // Pass all props
    // it will make child Components rerender by all props update
    <Header {...props}/>

createContext 建立資料

首先必須先在需要使用的元件內,先執行React.createContext建立一個context,其中參數defaultValue只會在沒有 Provider 傳遞value才會使用到。

  • React.createContext return object
    // React.createContext(defaultValue);
    const MyContext = React.createContext({isLogin: false});

    $$typeof: Symbol(react.context)
    Consumer: {$$typeof: Symbol(react.context), _context: {…}, _calculateChangedBits: null, …}
    Provider: {$$typeof: Symbol(react.provider), _context: {…}}
    _calculateChangedBits: null
    _currentRenderer: {}
    _currentRenderer2: null
    _currentValue: {isLogin: false}
    _currentValue2: {isLogin: false}
    _threadCount: 0
    __proto__: Object

Provider 提供value

調用 createContext 後,回傳的物件會帶有Provider、Consumer 元件,Provider 可以提供value,給相對應最接近的 Consumer 使用value,最特別的是 Provider 更新value後,會觸發相對應的 Consumer 更新元件,並且無視 shouldComponentUpdate 限制 (這在舊版Context無法達到)。

記得要 export React.createContext 回傳值,讓其他元件可以直接 import 使用 Consumer。還有提醒要注意 Provider 的 update 狀態,如果Provider 的元件會頻繁更新,但 Provider 的value 會每次都是新物件,會促使有 Cosumer 的元件每次都 update。

  • 當元件 rerender 會同時更新 Consumer 調用的元件

    // isLogin will forever new one
    render() {
    return (
    <MyContext.Provider value={{ isLogin: isLogin }}>
  • 傳遞的值保持同一參考 MyContext Provider

    export const MyContext = React.createContext({ isLogin: false });

    export default class Container extends Component {
    state = {
    isLoginStatus: { isLogin: true }
    };

    render() {
    const { isLoginStatus } = this.state;
    return (
    <MyContext.Provider value={isLoginStatus}>
    ...

Consumer 提取 value

Consumer 元件可以獲取 context 資料,假設沒有最接近的 Provider 提供value,Cosumer 會取到 createContext 的 defaultvalue。若有Provider提供值,則是會保持訂閱更新,也就是達到跨元件同步資料,並update component。

import React from "react";
import { MyContext } from "../Container";

export default function Account() {
return (
<div className="account">
<MyContext.Consumer>
{({ isLogin }) => {
return isLogin ? "Logout" : "Login";
}}
</MyContext.Consumer>
</div>
);
}

Consumer codesandbox

context更新 rerender取用元件

context 的 Provider 更新value時,會一起更新 context Consumer 的取用元件,並且無視於 shouldComponentUpdate。

Consumer shouldComponentUpdate codesandbox

contextType 取值

contextType 是直接在 react 的 component 的 instance 再加上 context,所以只能用在 class Component,一個元件只能使用一個 context。

import React, { Component } from "react";
import MyContext from "../context/MyContext";

export default class Account extends Component {
static contextType = MyContext;
render() {
const { isLogin, setLogin } = this.context;
return (
<div className="account">
<div>
{isLogin ? "Logout" : "Login"}
<button onClick={setLogin}>toggleLogin</button>
</div>
</div>
);
}
}

我在這邊有遇到一個問題,在 Container component export context,並在 Account 引用 Container export 的 context 時,會發生我取不到值得問題,這是因為循環依賴的關係,在我們 Account 引用 Container 內的 MyContext 時,ES6 只會是 referrence MyContext undefined 狀態,實際在 Container 還尚未建立 createContext,這個 Account 又會再初始化階段就執行 MyContext,導致拿到 empty object。

解法就是獨立建 MyContext ,解除與 Container 關係,就可以避免掉循環依賴的問題。至於 Consumer 會沒有問題,因為Consumer是在render時才會調用參考,所以會拿到正確的值。

// Container File
import Account from "./components/Account";
export const MyContext = React.createContext({
isLogin: false
});
export default class Container extends Component {
state = {
isLogin: true
};
...
<MyContext.Provider value={{ isLogin: isLogin, setLogin: this.setLogin }}>
<Account />
</MyContextProvider>
}

// Account File

import MyContext from "../Container";

export default class Account extends Component {
// MyContext undefined
static contextType = MyContext;
render() {
// empty object
console.log(this.context);
...

Dan神表示: React contextType undefined GitHub issue

how-to-analyze es6 circular-dependencies

Hooks useContext

React Hooks 有可以直接調用 Context 的方法,useContext 與 Consumer 特性相似,當沒有 Provider 提供 value,就會以 defaultValue為值,提醒有用到 useContext 的元件當value更新時皆會 rerender,rerender效能不好的話,建議搭配 Memo 來做 memorize。

const value = useContext(MyContext);

Preventing rerenders with React.memo and useContext hook.
Preventing rerenders with React.memo and useContext hook.

import React, { useContext, useMemo } from "react";
import MyContext from "../context/MyContext";

export default function Account() {
const { isLogin, setLogin } = useContext(MyContext);
return useMemo(() => {
return (
<div className="account">
<div>
{isLogin ? "Logout" : "Login"}
<button onClick={setLogin}>toggleLogin</button>
</div>
</div>
);
}, [isLogin, setLogin]);
}

useContext with useMemo codesandbox

心得

會特別研究寫關於 context API 內容,是因為目前專案幾乎都沒用到,多半還是以 redux 居多,redux 更新版hooks也有 useSelector,也是非常好用,雖然常聽到 useReucer、useContext 幾乎可以取代redux。

但 redux 有極好用的 debug 工具,devtool 觀看變化、history、dispatch,這些都是無法取代的功能。與夥伴討論過後,認為某些無狀態不需要更新值,我們才會考慮用 context API,因為不需要update,也沒有隨之的監控更新需求。