WEBKT

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

129 0 0 0

在现代 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列表筛选排序分页

评论点评