此篇接續上篇:
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
若對本文有興趣或有疑問,歡迎隨時提問喔!