(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
在過去幾天,我們都是用map
來製造MenuItem
。但如果你眼尖的話,應該會注意到在console噴出了這個東西
照字面翻的話就是React希望我們給MenuItem一個叫做key
的props,且每個MenuItem的key
最好都是不一樣的。
為什麼需要這個props呢?
現在我們先把前面的Menu系列的元件改造一下:
useContext
也就是程式碼長這樣:
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>
);
}
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後,按下這個新增用的按鍵,接著你會看到:
為什麼會這樣呢?
這是因為當陣列元素的索引位置被改變時,React會認為其製造出來的陣列元素也被改變。這裡我們的元素都各往後移動了一個索引位置,所以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會希望我們綁一個key 在MenuItem 上。 |
同時,我們也不應該拿元件在陣列中的索引值當作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 ) |
因為當單一wording被改變時,對應到的該單一元件本來就應該要重新渲染,所以在這個case我們就能拿來當key的值。實際上最好應該要有一個uuid
之類的東西。
現在,我們如果把wording當成key綁在MenuItem上
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