iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 23
2
Modern Web

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

【Day.23】React效能 - 用key避免陣列元件的重複渲染

(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問


在過去幾天,我們都是用map來製造MenuItem。但如果你眼尖的話,應該會注意到在console噴出了這個東西

照字面翻的話就是React希望我們給MenuItem一個叫做key的props,且每個MenuItem的key最好都是不一樣的。

為什麼需要這個props呢?

陣列元素的重複渲染問題

現在我們先把前面的Menu系列的元件改造一下:

  • 讓MenuItem改成用一個state製造
  • 移除可能會讓MenuItem重複渲染的useContext
  • 用useMemo和memo防止MenuItem重複渲染
  • 新增一個按鍵,按下去之後,讓「用來製造MenuItem的state」的最前面新增一筆資料。

也就是程式碼長這樣:

  • src/page/MenuPage.js
import React, {useState,useMemo} from 'react';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';

let menuItemWording=[
    "Like的發問",
    "Like的回答",
    "Like的文章",
    "Like的留言"
];

const MenuPage = () =>{
    const [isOpen, setIsOpen] = useState(true);
    const [menuItemData, setMenuItemData] = useState(menuItemWording);
    let menuItemArr = useMemo(()=> 
        menuItemData.map(
            (wording) => <MenuItem text={wording}/>
        ),[menuItemData]);

    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: setIsOpen
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
            <button onClick={()=>{
                let menuDataCopy = ["測試資料"].concat(menuItemData);
                setMenuItemData(menuDataCopy); 
            }}>更改第一個menuItem</button>
        </OpenContext.Provider>
    );
}
  • src/component/MenuItem.js
import React, {memo} from 'react';

const menuItemStyle = {
    marginBottom: "7px",
    paddingLeft: "26px",
    listStyle: "none"
};

function MenuItem(props){
    return  <li style={menuItemStyle}>{props.text}</li>;
}

export default memo(MenuItem);

接著開啟dev tool的Profile後,按下這個新增用的按鍵,接著你會看到:

所有的MenuItem都被重新渲染了

為什麼會這樣呢?

這是因為當陣列元素的索引位置被改變時,React會認為其製造出來的陣列元素也被改變。這裡我們的元素都各往後移動了一個索引位置,所以React就把元素重新渲染了。

用key讓React認得陣列中的元件

key是React用來辨識陣列元件、決定是否要重新渲染的工具。當陣列元件被改變,React會去比較「同key值的元件」和上次渲染時的值一不一樣,不一樣的時候才會重新渲染該元件。以下表為例,因為a、b、c、d對應到的props都和前一次一樣,所以React不會重新渲染他們。

舊(id) 新(id)
A( a ) Z( z )
B( b ) A( a )
C( c ) B( b )
D( d ) C( c )
D( d )
這也是為什麼剛剛React會希望我們綁一個keyMenuItem上。

同時,我們也不應該拿元件在陣列中的索引值當作key,因為以剛剛的例子來說,每個元件的索引值都往後1了。所以雖然除了在開頭新增了一個元件外,其他元件都沒有被改變,不應該被重新渲染,但如果你使用了索引值當作key,相同key對應到的內容就不一樣了,那等於沒有放key的狀況。

以下方為例,1對應到的元件props從B變成A,所以React會重新渲染他,以此類推A~D都會重新渲染。

舊(id) 新(id)
A( 0 ) Z( 0 )
B( 1 ) A( 1 )
C( 2 ) B( 2 )
D( 3 ) C( 3 )
D( 4 )

加入key到程式碼中

因為當單一wording被改變時,對應到的該單一元件本來就應該要重新渲染,所以在這個case我們就能拿來當key的值。實際上最好應該要有一個uuid之類的東西。

現在,我們如果把wording當成key綁在MenuItem上

  • src/page/MenuPage.js
import React, {useState,useMemo} from 'react';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';

let menuItemWording=[
    "Like的發問",
    "Like的回答",
    "Like的文章",
    "Like的留言"
];

const MenuPage = () =>{
    const [isOpen, setIsOpen] = useState(true);
    const [menuItemData, setMenuItemData] = useState(menuItemWording);
    let menuItemArr = useMemo(()=>
            menuItemData.map((wording) => 
                <MenuItem 
                    text={wording} 
                    key={wording}
                />
            ),[menuItemData]);

    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: setIsOpen
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
            <button onClick={()=>{
                let menuDataCopy = ["測試資料"].concat(menuItemData);
                setMenuItemData(menuDataCopy); 
            }}>更改第一個menuItem</button>
        </OpenContext.Provider>
    );
}

export default MenuPage;

重新執行並監聽效能,你就會發現只有新進來的MenuItem被重新渲染了。

參考資料

官方資料
為什麼不要總是直接用 array 的 index 當 React Component 的 Key


上一篇
【Day.22】React效能 - 如何處理useContext的效能問題
下一篇
【Day.24】React效能 - 用lazy和Suspense來動態載入元件
系列文
從比入門再往前一點開始,一直到深入React.js30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言