WEBKT

React 实现优雅的 Github Issue 列表:筛选、排序与分页的最佳实践

15 0 0 0

1. 组件结构设计

2. 状态管理

3. 数据获取与处理

4. 筛选功能实现

5. 排序功能实现

6. 分页功能实现

7. 性能优化

8. 完整代码示例

9. 总结

在现代 Web 应用中,列表展示是一个非常常见的需求。如果数据量较大,我们通常需要提供筛选、排序和分页功能,以提升用户体验。本文将以实现一个类似 Github Issue 列表为例,探讨如何使用 React 优雅地实现这些功能。

1. 组件结构设计

首先,我们需要将整个 Issue 列表拆分成几个核心组件:

  • IssueList: 负责渲染整个 Issue 列表,包括列表头和列表项。
  • IssueItem: 负责渲染单个 Issue,展示 Issue 的标题、状态、创建时间等信息。
  • FilterBar: 负责提供筛选功能,允许用户根据状态、标签等条件筛选 Issue。
  • SortBar: 负责提供排序功能,允许用户根据创建时间、更新时间等字段对 Issue 进行排序。
  • Pagination: 负责提供分页功能,允许用户切换到不同的页面。

这样的组件结构具有良好的可维护性和可复用性。每个组件只负责特定的功能,易于理解和修改。

2. 状态管理

Issue 列表的状态包括:

  • issues: Issue 数据列表。
  • filter: 筛选条件,例如 { state: 'open', label: 'bug' }
  • sort: 排序字段和排序方向,例如 { field: 'createdAt', order: 'desc' }
  • page: 当前页码。
  • pageSize: 每页显示的 Issue 数量。

我们可以使用 React 的 useState hook 来管理这些状态:

import React, { useState, useEffect, useCallback } from 'react';
function IssueList() {
const [issues, setIssues] = useState([]);
const [filter, setFilter] = useState({});
const [sort, setSort] = useState({ field: 'createdAt', order: 'desc' });
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [totalCount, setTotalCount] = useState(0); // 总Issue数量
// ...
}

3. 数据获取与处理

我们需要一个函数来获取 Issue 数据,并根据筛选、排序和分页条件进行处理。为了模拟真实场景,我们假设从 API 获取数据:

const fetchIssues = async (filter, sort, page, pageSize) => {
// 模拟 API 请求
const response = await fetch(`/api/issues?${new URLSearchParams({
...filter,
_sort: sort.field,
_order: sort.order,
_page: page,
_limit: pageSize
}).toString()}`);
const data = await response.json();
const total = response.headers.get('X-Total-Count'); // 从header获取总数
return { issues: data, total: parseInt(total || '0', 10) };
};

useEffect hook 可以用于在组件挂载时和状态更新时获取数据:

useEffect(() => {
const loadData = async () => {
const { issues: fetchedIssues, total } = await fetchIssues(filter, sort, page, pageSize);
setIssues(fetchedIssues);
setTotalCount(total);
};
loadData();
}, [filter, sort, page, pageSize]);

4. 筛选功能实现

FilterBar 组件需要提供用户界面来设置筛选条件。当筛选条件发生变化时,我们需要更新 filter 状态:

function FilterBar({ onFilterChange }) {
const [state, setState] = useState('');
const [label, setLabel] = useState('');
const handleStateChange = (e) => {
setState(e.target.value);
};
const handleLabelChange = (e) => {
setLabel(e.target.value);
};
const handleApplyFilter = () => {
onFilterChange({ state, label });
};
return (
<div>
<select value={state} onChange={handleStateChange}>
<option value="">All States</option>
<option value="open">Open</option>
<option value="closed">Closed</option>
</select>
<input type="text" placeholder="Label" value={label} onChange={handleLabelChange} />
<button onClick={handleApplyFilter}>Apply Filter</button>
</div>
);
}
function IssueList() {
// ...
const handleFilterChange = useCallback((newFilter) => {
setFilter(newFilter);
setPage(1); // Reset page to 1 when filter changes
}, []);
return (
<div>
<FilterBar onFilterChange={handleFilterChange} />
{/* ... */}
</div>
);
}

5. 排序功能实现

SortBar 组件需要提供用户界面来选择排序字段和排序方向。当排序条件发生变化时,我们需要更新 sort 状态:

function SortBar({ onSortChange }) {
const [field, setField] = useState('createdAt');
const [order, setOrder] = useState('desc');
const handleFieldChange = (e) => {
setField(e.target.value);
};
const handleOrderChange = (e) => {
setOrder(e.target.value);
};
const handleApplySort = () => {
onSortChange({ field, order });
};
return (
<div>
<select value={field} onChange={handleFieldChange}>
<option value="createdAt">Created At</option>
<option value="updatedAt">Updated At</option>
</select>
<select value={order} onChange={handleOrderChange}>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
<button onClick={handleApplySort}>Apply Sort</button>
</div>
);
}
function IssueList() {
// ...
const handleSortChange = useCallback((newSort) => {
setSort(newSort);
setPage(1); // Reset page to 1 when sort changes
}, []);
return (
<div>
<SortBar onSortChange={handleSortChange} />
{/* ... */}
</div>
);
}

6. 分页功能实现

Pagination 组件需要提供用户界面来切换页面。当页码发生变化时,我们需要更新 page 状态:

function Pagination({ currentPage, totalCount, pageSize, onPageChange }) {
const totalPages = Math.ceil(totalCount / pageSize);
const handlePageClick = (pageNumber) => {
onPageChange(pageNumber);
};
const pageNumbers = [];
for (let i = 1; i <= totalPages; i++) {
pageNumbers.push(i);
}
return (
<div>
{pageNumbers.map((pageNumber) => (
<button key={pageNumber} onClick={() => handlePageClick(pageNumber)} disabled={currentPage === pageNumber}>
{pageNumber}
</button>
))}
</div>
);
}
function IssueList() {
// ...
const handlePageChange = useCallback((newPage) => {
setPage(newPage);
}, []);
return (
<div>
{/* ... */}
<Pagination
currentPage={page}
totalCount={totalCount}
pageSize={pageSize}
onPageChange={handlePageChange}
/>
</div>
);
}

7. 性能优化

  • useCallback: 使用 useCallback hook 缓存事件处理函数,避免不必要的组件重新渲染。
  • useMemo: 使用 useMemo hook 缓存计算结果,例如筛选后的 Issue 列表,避免重复计算。
  • React.memo: 使用 React.memo 高阶组件对 IssueItem 组件进行 memoization,只有当 props 发生变化时才重新渲染。
  • 虚拟化 (Virtualization): 对于非常大的列表,可以考虑使用虚拟化技术,例如 react-windowreact-virtualized,只渲染可见区域的 Issue,提升性能。

8. 完整代码示例

import React, { useState, useEffect, useCallback } from 'react';
function IssueList() {
const [issues, setIssues] = useState([]);
const [filter, setFilter] = useState({});
const [sort, setSort] = useState({ field: 'createdAt', order: 'desc' });
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [totalCount, setTotalCount] = useState(0);
const fetchIssues = async (filter, sort, page, pageSize) => {
const response = await fetch(`/api/issues?${new URLSearchParams({
...filter,
_sort: sort.field,
_order: sort.order,
_page: page,
_limit: pageSize
}).toString()}`);
const data = await response.json();
const total = response.headers.get('X-Total-Count');
return { issues: data, total: parseInt(total || '0', 10) };
};
useEffect(() => {
const loadData = async () => {
const { issues: fetchedIssues, total } = await fetchIssues(filter, sort, page, pageSize);
setIssues(fetchedIssues);
setTotalCount(total);
};
loadData();
}, [filter, sort, page, pageSize]);
const handleFilterChange = useCallback((newFilter) => {
setFilter(newFilter);
setPage(1);
}, []);
const handleSortChange = useCallback((newSort) => {
setSort(newSort);
setPage(1);
}, []);
const handlePageChange = useCallback((newPage) => {
setPage(newPage);
}, []);
return (
<div>
<FilterBar onFilterChange={handleFilterChange} />
<SortBar onSortChange={handleSortChange} />
<IssueListItems issues={issues} />
<Pagination
currentPage={page}
totalCount={totalCount}
pageSize={pageSize}
onPageChange={handlePageChange}
/>
</div>
);
}
const IssueListItems = React.memo(function IssueListItems({ issues }) {
return (
<ul>
{issues.map((issue) => (
<IssueItem key={issue.id} issue={issue} />
))}
</ul>
);
});
function IssueItem({ issue }) {
return (
<li>
{issue.title} - {issue.state}
</li>
);
}
function FilterBar({ onFilterChange }) {
const [state, setState] = useState('');
const [label, setLabel] = useState('');
const handleStateChange = (e) => {
setState(e.target.value);
};
const handleLabelChange = (e) => {
setLabel(e.target.value);
};
const handleApplyFilter = () => {
onFilterChange({ state, label });
};
return (
<div>
<select value={state} onChange={handleStateChange}>
<option value="">All States</option>
<option value="open">Open</option>
<option value="closed">Closed</option>
</select>
<input type="text" placeholder="Label" value={label} onChange={handleLabelChange} />
<button onClick={handleApplyFilter}>Apply Filter</button>
</div>
);
}
function SortBar({ onSortChange }) {
const [field, setField] = useState('createdAt');
const [order, setOrder] = useState('desc');
const handleFieldChange = (e) => {
setField(e.target.value);
};
const handleOrderChange = (e) => {
setOrder(e.target.value);
};
const handleApplySort = () => {
onSortChange({ field, order });
};
return (
<div>
<select value={field} onChange={handleFieldChange}>
<option value="createdAt">Created At</option>
<option value="updatedAt">Updated At</option>
</select>
<select value={order} onChange={handleOrderChange}>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
<button onClick={handleApplySort}>Apply Sort</button>
</div>
);
}
function Pagination({ currentPage, totalCount, pageSize, onPageChange }) {
const totalPages = Math.ceil(totalCount / pageSize);
const handlePageClick = (pageNumber) => {
onPageChange(pageNumber);
};
const pageNumbers = [];
for (let i = 1; i <= totalPages; i++) {
pageNumbers.push(i);
}
return (
<div>
{pageNumbers.map((pageNumber) => (
<button key={pageNumber} onClick={() => handlePageClick(pageNumber)} disabled={currentPage === pageNumber}>
{pageNumber}
</button>
))}
</div>
);
}
export default IssueList;

注意:

  • 上述代码只是一个简化示例,实际应用中需要根据具体需求进行调整。
  • API 地址 /api/issues 只是一个示例,需要替换成真实的 API 地址。
  • 样式和布局可以根据具体需求进行自定义。

9. 总结

本文介绍了如何使用 React 优雅地实现一个类似 Github Issue 列表,包括筛选、排序和分页功能。通过合理的组件结构设计、状态管理和性能优化,我们可以构建出高效、可维护的列表组件。希望本文对你有所帮助!

码农小李 ReactIssue列表筛选排序分页

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/10225