You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

针对服务端筛选+分页场景,实现可扩展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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.16 07:14:40