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

如何用react-query实现无限滚动?解决Hook嵌套及渲染疑问

React Query 实现无限滚动的优化方案及疑问解答

问题描述

场景

项目需要实现无限滚动,当前使用原生JavaScript Fetch API,希望替换为React Query。

问题

React Query以Hook形式存在,无法嵌套在useCallback中,当前手动管理分页数据的写法无法直接适配。

当前实现代码

function RQInfiniteScrollTable({src}:{src:string}){
    const [page,setPage] = useState(1);
    const [pageData,setPageData]:[pageData:Array<object>,setPageData:any] = useState([]);

    const loadMore = useCallback(async () => {
        const new_page = page + 1;
        setPage(new_page);
        const buffer: Array<object> = [...pageData];
        let moreData:Response|null = await fetch([src,new_page].join("/").trim());
        let moreData_json:object|null = await moreData.json();
        if(moreData_json === null){return;}
        buffer.push(moreData_json);
        setPageData(buffer);
        return ()=>{moreData = null; moreData_json = null;};
    },[pageData]);

    const products:Array<React.ReactNode> = [];
    // @ts-ignore
    pageData.forEach(({title,image,price,category,description},index):void=>{
        products.push(
        <div key={index} className={"infinite-item-card"}>
            <div className={"w-full h-64 relative"}>
                <Image src={image} alt={title} layout={"fill"} objectPosition={"cover"} objectFit={"cover"}/>
            </div>
            <h1>{title}</h1>
            <p>{price}</p>
            <p>{category}</p>
            <p>{description}</p>
        </div>
        );
    });
    return(
    <div className={"w-full grid justify-items-center"}>
        <h1 className={"text-2xl w-fit my-8"}>{"Infinite Scroller"}</h1>
        <div className={"grid"}>{products}</div>
        <InfiniteScroller loadMore={loadMore}/> {/* 当元素交叉时调用loadMore */}
    </div>
    );
}

疑问

  1. 如何优化适配React Query实现无限滚动?
  2. useQuery的查询结果是否会触发组件重渲染?

解答

1. 用React Query的useInfiniteQuery实现无限滚动

React Query专门提供了useInfiniteQuery钩子处理无限滚动/分页场景,无需手动管理page、pageData状态,还自带缓存、重复请求拦截、状态管理等能力。重构后的代码如下:

import { useInfiniteQuery } from '@tanstack/react-query';

function RQInfiniteScrollTable({ src }: { src: string }) {
    // 使用useInfiniteQuery获取分页数据
    const {
        data,
        fetchNextPage,
        hasNextPage,
        isFetchingNextPage,
        error
    } = useInfiniteQuery({
        queryKey: ['products', src], // 缓存键,确保不同src对应不同缓存
        queryFn: async ({ pageParam = 1 }) => {
            // pageParam是当前要请求的页码,默认从1开始
            const response = await fetch(`${src}/${pageParam}`.trim());
            if (!response.ok) throw new Error('请求失败');
            return response.json();
        },
        // 告诉React Query如何判断下一页的页码
        getNextPageParam: (lastPage, allPages) => {
            // 根据实际接口返回逻辑调整,示例:若最后一页有数据则返回下一页页码,否则返回undefined
            return lastPage.length > 0 ? allPages.length + 1 : undefined;
        }
    });

    // 处理加载错误
    if (error) return <div className="text-red-500">加载失败:{(error as Error).message}</div>;

    // 拼接所有分页数据
    const allProducts = data?.pages.flat() || [];

    // 渲染产品列表
    const products = allProducts.map((product, index) => (
        <div key={`${product.id}-${index}`} className="infinite-item-card"> {/* 用id+index做key更稳定 */}
            <div className="w-full h-64 relative">
                <Image 
                    src={product.image} 
                    alt={product.title} 
                    layout="fill" 
                    objectPosition="cover" 
                    objectFit="cover" 
                />
            </div>
            <h1>{product.title}</h1>
            <p>{product.price}</p>
            <p>{product.category}</p>
            <p>{product.description}</p>
        </div>
    ));

    // 定义loadMore:调用fetchNextPage,同时避免重复请求
    const loadMore = () => {
        if (hasNextPage && !isFetchingNextPage) {
            fetchNextPage();
        }
    };

    return (
        <div className="w-full grid justify-items-center">
            <h1 className="text-2xl w-fit my-8">Infinite Scroller</h1>
            <div className="grid">{products}</div>
            {/* 只有还有下一页时才渲染加载触发组件 */}
            {hasNextPage && <InfiniteScroller loadMore={loadMore} />}
            {/* 可选:显示加载状态 */}
            {isFetchingNextPage && <div className="my-4">加载中...</div>}
        </div>
    );
}

关键优化点:

  • 用useInfiniteQuery替代手动状态管理,自动处理分页数据拼接与状态控制
  • 通过queryKey实现缓存复用,相同请求直接读取缓存,减少无效请求
  • getNextPageParam自动判断是否存在下一页,无需手动维护page变量
  • fetchNextPage是React Query提供的稳定方法,自带重复请求拦截,解决了Hook无法嵌套useCallback的问题

2. useQuery的查询结果会触发组件重渲染吗?

会触发,但React Query做了智能重渲染优化,仅在以下场景触发:

  • 查询数据发生实际变化(比如新分页数据返回)
  • 查询状态改变(如从idle变为loading、出现error)
  • 缓存数据被手动更新(如调用invalidateQueries刷新)

React Query内部通过优化状态引用逻辑,避免无意义重渲染:如果请求返回数据与缓存完全一致,组件不会更新;同时返回的data、isLoading等状态都是稳定引用,不会因组件重渲染导致无效更新。


内容的提问来源于stack exchange,提问作者レノキ乇1ᑌᑭ

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 00:24:54