iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 22
1
Modern Web

從比入門再往前一點開始,一直到深入React.js系列 第 22

【Day.22】React效能 - 如何處理useContext的效能問題

前面我們提過,Context api雖然方便,但是每一次context更新時都會迫使「有使用useContext取得該context」的元件更新。這在專案有規模時會造成很嚴重的效能問題。

學了這麼多效能處理的方法,那麼我們到底要怎麼處理useContext的效能問題呢?

React官方提出了三種原生的解決方案建議:

1. 拆分Context

這個作法是React官方最推薦的。

以我們的範例來說,本來我們把isOpensetIsOpen都包進去同一個Context。並讓MenuItem和Menu個別引入。但其實MenuItem只需要setIsOpen,所以我們應該要把isOpensetIsOpen分成兩個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;

2. 分層與使用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;

但是這樣的缺點也很明顯,會多出很多無用的空元件。

3. 子元件使用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 與 Redux

Context API的本意並不是讓我們進行多層state的管理,而是讓多個共用資料的元件能夠方便隨著資料的更動而被更新。也因為這樣,React才會讓使用useContext的元件都重新渲染,第三方的狀態管理工具也並沒有隨著Context API的出現而消失。

Redux就是最通用的第三方狀態管理函式庫,我們會在之後的文章中介紹。


上一篇
【Day.21】React效能 - 用useMemo避免函式非必要的執行
下一篇
【Day.23】React效能 - 用key避免陣列元件的重複渲染
系列文
從比入門再往前一點開始,一直到深入React.js30

尚未有邦友留言

立即登入留言