此篇接續上篇:
A6 React 和 Vue 實作表格元件:排序、搜尋與分頁功能詳解
const columns = [
{ accessor: 'price', label: '售價' },
{ accessor: 'name', label: '品名' },
{ accessor: 'onsale', label: '在架上', format: value => (value ? '✔️' : ' ') },
//......
]
跟後端要資料:
const [tableData, setTableData] = useState([]);
useEffect(() => {
axios.get('url/api')
.then(response => {
const cookie = response.data.cookie;
setTableData(cookie);
})
.catch(error => {
console.error('Error gathering data:', error);
});
}, []);
也可以繼續填充其他資料,進行拼接:
const [rows, setRows] = useState([]);
useEffect(()=>{
const newTableData = tableData.concat([
{
id: 1,
name: "potatochip 洋芋片",
price: '50',
onsale: true,
tag: 'salty crispy delicious',
rate: '⭐️⭐️⭐️⭐️',
expiryDate: '2025-01-01',
category: 'snack',
stock: 100
},
{
id: 2,
name: "chocolate 巧克力"
//......
},
//......
])
const newRows = newTableData.map((data, index) => {
return { ...data, key: index }
})
setRows(newRows);
}, [tableData]);
最後為每筆資料添加 key 值
為了讓模板盡量簡潔,我們可以分類,將標題、搜尋、排序、內容分開撰寫,直接的做法就是用組件的形式:
<div style={{ overflow: 'auto' }}>
<table className="table">
<thead className="thead">
<Title/>
<FilterInput/>
<SortBtn/>
</thead>
<tbody>
<Content/>
</tbody>
</table>
</div>
但是這實際上會有一些問題,只要有狀態更動,所有元件都會重新渲染。這會浪費資源,因為我們渲染的對象分別有 column 和 row,在更新表格資料時 column 作為架構是不變的,只有 row 會影響內容。
更嚴重的是,搜尋功能會出bug!由於input重新渲染,使用者每輸入一個字就被強制失焦。
幸好,有 useMemo 來幫我們解決問題!例如,我希望標題是根據 column 來決定是否重新渲染:
const Title = useMemo(() => {
return (
<tr className="tr">
{columns.map(column => {
return (
<th className="th" key={column.accessor}>
<span>{column.label}</span>
</th>
)
})}
</tr>
);
}, [columns]);
將模板作為變數儲存,useMemo 會幫你檢查 columns 是否更新,就能避免重新渲染。接著就能把 Table 元件改成以下形式:
<div style={{ overflow: 'auto' }}>
<table className="table">
<thead className="thead">
{Title}
{FilterInput}
{SortBtn}
</thead>
<tbody>
{Content}
</tbody>
</table>
</div>
還記得昨天完成的排序和搜尋功能嗎?我們用到以下兩個參數來管理狀態:
const [filters, setFilters] = useState({});
const [sort, setSort] = useState({ order: 'asc', orderBy: 'id' });
首先,我們實作搜尋元件的狀態管理:
const FilterInput = useMemo(() => {
return (
<tr className="tr">
{columns.map(column => {
return (
<th className="th" key={`${column.accessor}-search`}>
<label><input
className="input"
key={`${column.accessor}-search`}
type="search"
placeholder={`搜尋${column.label}`}
value={filters[column.accessor] || ""}
onChange={e => {
handleSearch(e.target.value, column.accessor)
}}
/></label>
</th>
)
})}
</tr>
);
}, [columns, filters]);
在這裡,我們檢查是否有值,來決定加入或刪除搜尋項:
const handleSearch = (value, accessor) => {
setActivePage(1)
if (value) {
setFilters(prevFilters => ({
...prevFilters,
[accessor]: value,
}))
} else {
setFilters(prevFilters => {
const updatedFilters = { ...prevFilters }
delete updatedFilters[accessor]
return updatedFilters
})
}
}
在這部分,我們同樣需要進行狀態管理,以實現排序功能:
const SortBtn = useMemo(() => {
return (
<tr className="tr">
{columns.map(column => {
return (
<th className="th" key={`${column.accessor}-search`}>
<button className="button" onClick={() => {
handleSort(column.accessor)
}>{
(column.accessor === sort.orderBy)
? (sort.order === 'asc' ? '升序🟢' : '降序🔴') : '️排序⚪'
}</button>
</th>
)
})}
</tr>
);
}, [columns, sort]);
在這裡,我們將表格設定為第一頁,並指定新的排序對象:
const handleSort = accessor => {
setActivePage(1)
setSort(prevSort => ({
order: prevSort.order === 'desc' && prevSort.orderBy === accessor ? 'asc' : 'desc',
orderBy: accessor,
}))
}
如果排序對象相同(使用者點擊兩次),就改成升序排列
最後的收尾,讓我們把內容完成吧!昨天我們經過搜尋、排序、分頁一系列操作後,得到了 calculatedRows,用它進行渲染:
恭喜大家撐過以上嵌套地獄,輕鬆的要來啦!只要用 v-for 指令,就能輕鬆歷遍資料,一口氣完成:
很短吧!而且可讀性還蠻高的,接下來的邏輯和 React 完全一致,結合昨天的內容,這樣讓狀態管理就輕鬆解決拉!
恭喜大家!透過本文學習了在 React 和 Vue 中實現一個完整的表格元件,並且具備排序、搜尋和分頁的功能,實作過程中也經歷了許多的挑戰,但最終獲得了成果。
如果感興趣,可以參考 Github 上的原始碼:
Table.jsx
Table.vue
Pagination.vue
若對本文有興趣或有疑問,歡迎隨時提問喔!