今天我們要把昨天的 react-window 觀念,實際應用到系統的「寶可夢交換列表」中。在這個對話框裡,我們需要一口氣列出玩家擁有的所有寶可夢,結合虛擬滾動後,我們還要搭配一個效能殺手鐧:React.memo。
React.memo?在虛擬滾動中,當使用者快速向下滑動,React 必須在毫秒級別內不斷地「摧毀上方看不到的 Row」並「建立下方新出現的 Row」。
如果我們沒對 Row 進行快取,當上層組件 (List) 的狀態改變時,清單裡所有的 Row 都會被迫重新渲染 (Re-render)。
透過 React.memo 把每一列 (Row) 包起來,可以告訴 React:「只要這列的 props (例如這隻寶可夢的資料) 沒有改變,就直接拿上次畫好的結果來用,不要重畫!」這正是長列表效能優化的最後一哩路。
在 src/components/admin/TradePokemonDialog.tsx 中,我們完美結合了這兩項技術:
import React, { memo } from 'react';
import { FixedSizeList } from 'react-window';
import { Box, Stack, Typography } from '@mui/material';
// 1. 使用 React.memo 包裝 Row,避免不必要的重新渲染
const TradePokemonRow = memo(({ index, style, data }: any) => {
const { filtered, selectedCounts } = data;
const group = filtered[index];
if (!group) return null;
return (
// 必須把 style 傳給最外層的容器,這是 react-window 絕對定位的關鍵!
<Box style={style} sx={{ borderBottom: '1px solid #eee' }}>
<Stack direction="row" sx={{ p: 2 }}>
<Typography>{group.representative.pokemon_name}</Typography>
<Typography>選擇數量:{selectedCounts[group.key] || 0}</Typography>
</Stack>
</Box>
);
});
// 2. 在主件中使用 FixedSizeList
const TradePokemonList = ({ filtered, selectedCounts }) => {
return (
<FixedSizeList
height={400} // 列表可視高度
width="100%" // 列表可視寬度
itemSize={60} // 每一列的固定高度
itemCount={filtered.length} // 資料總筆數
itemData={{ filtered, selectedCounts }} // 傳遞給 Row 的動態資料
>
{TradePokemonRow}
</FixedSizeList>
);
};


在交換寶可夢頁面使用交換功能,選擇要交換的帳號後開啟「從庫存選擇」,展開可以交換的寶可夢庫存。
透過 React Window,載入多筆資料依然順暢。
我們成功排除了 DOM 節點過多與無謂渲染所造成的效能地雷!明天,我們要為這個超長列表加上「搜尋與過濾功能」,讓使用者能一秒找到他想找的寶可夢!