针对服务端筛选+分页场景,实现可扩展React ContextProvider以处理数据获取与筛选的最佳方案
针对服务端筛选+分页场景,实现可扩展React ContextProvider以处理数据获取与筛选的最佳方案
嘿,我看到你在做一个需要服务端筛选+分页的React项目,还尝试用Context但踩了坑,刚好我对这种场景熟得很,来给你捋捋一套靠谱、可扩展的方案~
首先先说说你当前useFetch钩子的几个小问题:你把URL参数拼接放在了组件渲染阶段,这会导致每次渲染都重新拼接,要是参数是对象的话还可能因为引用变化触发不必要的请求;另外错误处理也有点不完整,请求失败的情况没覆盖全。先给你重构一下这个钩子,让它更健壮:
import { useState, useEffect, useCallback } from 'react'; const useFetch = (url, queryParams) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [refreshTrigger, setRefreshTrigger] = useState(0); // 用useCallback包裹,避免每次渲染重新创建函数 const refreshData = useCallback(() => { setRefreshTrigger(prev => prev + 1); }, []); useEffect(() => { const fetchData = async () => { setLoading(true); setError(null); // 用AbortController处理请求取消,避免组件卸载后请求还在跑 const controller = new AbortController(); const signal = controller.signal; try { let fullUrl = url; if (queryParams) { // 兼容对象和URLSearchParams两种参数格式 const params = typeof queryParams === 'object' && !(queryParams instanceof URLSearchParams) ? new URLSearchParams(queryParams) : queryParams; fullUrl = `${url}?${params.toString()}`; } const response = await fetch(fullUrl, { signal }); if (!response.ok) { throw new Error(`请求失败:${response.statusText}`); } const result = await response.json(); setData(result); } catch (err) { // 排除手动取消的请求 if (err.name !== 'AbortError') { setError(err.message); } } finally { setLoading(false); } // 组件卸载时取消请求 return () => controller.abort(); }; fetchData(); }, [url, queryParams, refreshTrigger]); return { data, loading, error, refreshData }; };
这个重构后的钩子做了这些优化:
- 把参数处理移到了useEffect内部,避免渲染阶段的副作用
- 用
useCallback包裹refreshData,防止子组件不必要重渲染 - 加入了
AbortController处理请求取消,避免内存泄漏 - 完善了错误处理,区分了请求失败和手动取消的情况
接下来就是核心的可扩展Context Provider了,我们把筛选、分页、数据获取的状态统一管理起来,让子组件不用自己拼接参数、处理状态同步:
import { createContext, useContext, useState, useMemo } from 'react'; import useFetch from './useFetch'; const DataContext = createContext(); export const DataProvider = ({ children, baseUrl }) => { // 筛选条件状态,支持任意键值对扩展 const [filters, setFilters] = useState({}); // 分页状态,默认每页10条,第1页 const [pagination, setPagination] = useState({ page: 1, pageSize: 10 }); // 可选:排序状态,需要的话可以加上 const [sort, setSort] = useState({ field: 'id', direction: 'asc' }); // 用useMemo缓存请求参数,避免每次渲染重新生成 const queryParams = useMemo(() => { const params = new URLSearchParams(); // 添加筛选参数,忽略空值 Object.entries(filters).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { params.append(key, value); } }); // 添加分页参数 params.append('page', pagination.page); params.append('pageSize', pagination.pageSize); // 添加排序参数(如果需要) params.append('sortBy', sort.field); params.append('sortDir', sort.direction); return params; }, [filters, pagination, sort]); // 调用重构后的useFetch获取数据 const { data, loading, error, refreshData } = useFetch(baseUrl, queryParams); // 封装给子组件的操作方法,用useMemo缓存避免重渲染 const contextValue = useMemo(() => ({ data, loading, error, // 筛选相关方法 filters, // 完全替换筛选条件,自动重置页码到第1页 setFilters: (newFilters) => { setFilters(newFilters); setPagination(prev => ({ ...prev, page: 1 })); }, // 部分更新筛选条件,同样重置页码 updateFilters: (partialFilters) => { setFilters(prev => ({ ...prev, ...partialFilters })); setPagination(prev => ({ ...prev, page: 1 })); }, // 分页相关方法 pagination, setPagination: (newPagination) => { setPagination(newPagination); }, goToPage: (pageNumber) => { setPagination(prev => ({ ...prev, page: pageNumber })); }, // 排序相关方法(可选) sort, setSort: (newSort) => { setSort(newSort); setPagination(prev => ({ ...prev, page: 1 })); }, // 手动刷新数据 refreshData }), [data, loading, error, filters, pagination, sort, refreshData]); return ( <DataContext.Provider value={contextValue}> {children} </DataContext.Provider> ); }; // 自定义钩子,简化子组件调用Context的方式 export const useData = () => { const context = useContext(DataContext); if (!context) { throw new Error('useData必须在DataProvider内部使用哦'); } return context; };
这个Provider的优势很明显:
- 统一管理所有和数据相关的状态,子组件不用自己维护筛选、分页的状态
- 状态变化时自动同步请求参数,不用手动拼接URL
- 筛选、排序变化时自动重置页码,符合用户的使用预期(换筛选条件肯定想从第一页看)
- 提供了灵活的操作方法,后续要加新的筛选条件或者排序字段,直接扩展状态就行,不用改核心逻辑
接下来看看怎么在项目里用这套东西:
首先在你的根组件或者父组件里包裹DataProvider:
import { DataProvider } from './DataProvider'; function App() { return ( <DataProvider baseUrl="https://your-api-domain.com/api/data"> {/* 你的业务组件,比如筛选栏、数据列表、分页器 */} <FilterBar /> <DataList /> <PaginationControls /> </DataProvider> ); }
然后写筛选栏组件FilterBar,直接用useData钩子获取状态和方法:
import { useData } from './DataProvider'; function FilterBar() { const { filters, updateFilters } = useData(); return ( <div className="filter-bar"> <input type="text" placeholder="搜索关键词" value={filters.search || ''} onChange={(e) => updateFilters({ search: e.target.value })} /> <select value={filters.category || ''} onChange={(e) => updateFilters({ category: e.target.value })} > <option value="">所有分类</option> <option value="electronics">电子产品</option> <option value="clothing">服装</option> </select> </div> ); }
数据列表组件DataList:
import { useData } from './DataProvider'; function DataList() { const { data, loading, error } = useData(); if (loading) return <div className="loading">加载中...</div>; if (error) return <div className="error">出错啦:{error}</div>; if (!data?.items) return <div className="empty">暂无数据</div>; return ( <ul className="data-list"> {data.items.map(item => ( <li key={item.id} className="list-item"> <h3>{item.title}</h3> <p>{item.description}</p> </li> ))} </ul> ); }
分页器组件PaginationControls:
import { useData } from './DataProvider'; function PaginationControls() { const { pagination, goToPage, data } = useData(); const totalPages = data ? Math.ceil(data.totalCount / pagination.pageSize) : 0; return ( <div className="pagination"> <button onClick={() => goToPage(pagination.page - 1)} disabled={pagination.page === 1} className="page-btn" > 上一页 </button> <span className="page-info">第{pagination.page}页 / 共{totalPages}页</span> <button onClick={() => goToPage(pagination.page + 1)} disabled={pagination.page >= totalPages} className="page-btn" > 下一页 </button> </div> ); }
最后给你几个额外的优化小建议:
- 给筛选输入加防抖:比如用
useDebounce钩子,用户停止输入500ms后再发起请求,避免频繁调用API - 添加请求缓存:如果相同的参数已经请求过,可以缓存结果,不用重复请求
- 支持多条件组合筛选:比如范围筛选(价格区间),只需要在
updateFilters里传入对应的键值对就行,Provider会自动处理参数拼接
备注:内容来源于stack exchange,提问作者Abdullah Shekfeh
相关产品推荐
相关产品推荐

