(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
前面我們提過,Context api雖然方便,但是每一次context更新時都會迫使「有使用useContext取得該context」的元件更新。這在專案有規模時會造成很嚴重的效能問題。
學了這麼多效能處理的方法,那麼我們到底要怎麼處理useContext的效能問題呢?
這個作法是React官方最推薦的。
以我們的範例來說,本來我們把isOpen和setIsOpen都包進去同一個Context。並讓MenuItem和Menu個別引入。但其實MenuItem只需要setIsOpen,所以我們應該要把isOpen和setIsOpen分成兩個Context,這樣當isOpen被更新時,就不會強迫渲染MenuItem了。
import React from "react";
export const OpenContext = React.createContext(true);
export const SetOpenContext = React.createContext( ()=>{})
import React, {useState, useRef, useCallback, useMemo} from 'react';
import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext,SetOpenContext } from '../context/ControlContext';
let menuItemWording=[
    "Like的發問",
    "Like的回答",
    "Like的文章",
    "Like的留言"
];
const MenuPage = () =>{
    const [isOpen, setIsOpen] = useState(true);
    const renderCounter = useRef(0);
    renderCounter.current++;
    const handleClick = useCallback(() => {
        console.log("counter is " + renderCounter.current);
    },[renderCounter]);
    
    const menuItemArr = useMemo(()=> {
        return menuItemWording.map((wording) => <MenuItem text={wording}/>)
    },[]);
    return (
        <OpenContext.Provider value={isOpen}>
            <SetOpenContext.Provider value={setIsOpen}>
                <Menu title={"Andy Chang的like"}>
                    {menuItemArr}
                </Menu>
            </SetOpenContext.Provider>
        </OpenContext.Provider>
    );
}
export default MenuPage;
import React, {useContext} from 'react';
import { SetOpenContext } from '../context/ControlContext';
const menuItemStyle = {
    marginBottom: "7px",
    paddingLeft: "26px",
    listStyle: "none"
};
function MenuItem(props){
    const setIsOpen = useContext(SetOpenContext);
    return <li 
                style={menuItemStyle}
                onClick={()=>{setIsOpen(false)}}
            >
                {props.text}
            </li>;
}
export default MenuItem;
memo先前我們有提過,memo會去比對元件的props是不是有被改變。所以如果我們把context抽出來變成一個純用來傳資料的父元件,再用他包住本來的子元件,用props把要用的contex資料傳進去,最後再用memo包住子元件。
當context更新時,React就只會重複渲染那個空的父元件,子元件在綁定props的context沒變時就不會渲染。
import React, {memo, useContext} from 'react';
import { OpenContext } from '../context/ControlContext';
const menuItemStyle = {
    marginBottom: "7px",
    paddingLeft: "26px",
    listStyle: "none"
};
const MenuItemElement=memo((props)=>{
    return <li style={menuItemStyle}>{props.text}</li>;
});
function MenuItem(props){
    const isOpenUtil = useContext(OpenContext);
    const func = isOpenUtil.setIsOpenContext;
    return <MenuItemElement text={props.text} setIsOpen={func}/>;
}
export default MenuItem;

但是這樣的缺點也很明顯,會多出很多無用的空元件。
useMemo這個方法最直觀,就是用useMemo包住每個元件的回傳值,只在第二個相依變數array監聽會需要用到的context資料。這樣元件本身雖然會被重新執行,但useMemo回傳的會是上一次記憶的東西。
但也因為這樣,很多元件都要包useMemo,就等於你要讓React多記憶很多東西,所以也不被建議。
import React, {useMemo,useContext} from 'react';
import { OpenContext } from '../context/ControlContext';
const menuItemStyle = {
    marginBottom: "7px",
    paddingLeft: "26px",
    listStyle: "none"
};
const MenuItemElement=(props)=>{
    return <li style={menuItemStyle}>{props.text}</li>;
}
function MenuItem(props){
    const isOpenUtil = useContext(OpenContext);
    const setIsOpen = isOpenUtil.setIsOpenContext;
    return  useMemo(()=><li style={menuItemStyle} onClick={()=>{func(false)}}>{props.text}</li>,[setIsOpen]);
}
export default MenuItem;
Context API的本意並不是讓我們進行多層state的管理,而是讓多個共用資料的元件能夠方便隨著資料的更動而被更新。也因為這樣,React才會讓使用useContext的元件都重新渲染,第三方的狀態管理工具也並沒有隨著Context API的出現而消失。
Redux就是最通用的第三方狀態管理函式庫,我們會在之後的文章中介紹。