基于React-Query 4实现分页详情页的上下项切换及自动加载
实现TodoDetails组件的前后切换逻辑(支持自动加载分页)
现有基于React Query 4实现的分页项目列表(每页9项),需为TodoDetails组件实现以下功能:
- 点击「Previous」「Next」按钮逐个切换项目
- 切换到未加载页面的项目时,自动加载对应页面并展示目标项
核心思路
- 利用React Query的缓存机制,跟踪所有已加载的分页数据
- 在父组件(
Todos)中维护当前选中项目的ID,统一管理分页与选中状态 - 切换时先从缓存查找目标项目,若不存在则计算对应页码并触发加载,完成后切换到目标项
代码实现
1. 修改分页列表组件(Todos)
新增状态管理、缓存数据读取逻辑,并实现onPrevious/onNext函数:
import { useQuery, useQueryClient } from '@tanstack/react-query'; import React from 'react'; function Todos() { const [page, setPage] = React.useState(0); const [currentItemId, setCurrentItemId] = React.useState(null); const queryClient = useQueryClient(); const ITEMS_PER_PAGE = 9; const fetchProjects = (page = 0) => fetch(`/api/projects?page=${page}`).then(res => res.json()); const { isLoading, isError, error, data, isFetching, isPreviousData, } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), keepPreviousData: true }); // 获取所有已加载的项目缓存并合并为数组 const getAllLoadedProjects = () => { const projectQueries = queryClient.getQueriesData({ queryKey: ['projects'] }); return projectQueries.flatMap(([_, queryData]) => queryData?.projects || []); }; // 找到当前项目在总列表中的索引 const getCurrentItemIndex = () => { const allProjects = getAllLoadedProjects(); return allProjects.findIndex(item => item.id === currentItemId); }; // 上一项切换逻辑 const handlePrevious = () => { const currentIndex = getCurrentItemIndex(); if (currentIndex <= 0) return; const allProjects = getAllLoadedProjects(); setCurrentItemId(allProjects[currentIndex - 1].id); }; // 下一项切换逻辑 const handleNext = async () => { const allProjects = getAllLoadedProjects(); const currentIndex = getCurrentItemIndex(); const nextIndex = currentIndex + 1; // 下一项已在缓存中,直接切换 if (nextIndex < allProjects.length) { setCurrentItemId(allProjects[nextIndex].id); return; } // 无更多页面时终止 if (!data?.hasMore) return; // 计算目标页码并预加载 const targetPage = Math.floor(nextIndex / ITEMS_PER_PAGE); await queryClient.prefetchQuery({ queryKey: ['projects', targetPage], queryFn: () => fetchProjects(targetPage) }); // 加载完成后切换到目标项,同步当前页 const updatedProjects = getAllLoadedProjects(); if (nextIndex < updatedProjects.length) { setCurrentItemId(updatedProjects[nextIndex].id); setPage(targetPage); } }; // 获取当前选中的项目详情 const currentItem = getAllLoadedProjects().find(item => item.id === currentItemId); return ( <div> {/* 项目列表 */} {isLoading ? ( <div>Loading...</div> ) : isError ? ( <div>Error: {error.message}</div> ) : ( <div> {data.projects.map(project => ( <p key={project.id} style={{ fontWeight: project.id === currentItemId ? 'bold' : 'normal' }} onClick={() => setCurrentItemId(project.id)} > {project.name} </p> ))} </div> )} {/* 分页按钮 */} <span>Current Page: {page + 1}</span> <button onClick={() => setPage(old => Math.max(old - 1, 0))} disabled={page === 0} > Previous Page </button>{' '} <button onClick={() => { if (!isPreviousData && data.hasMore) { setPage(old => old + 1); } }} disabled={isPreviousData || !data?.hasMore} > Next Page </button> {isFetching ? <span> Loading...</span> : null}{' '} {/* 详情页组件 */} {currentItem && ( <TodoDetails item={currentItem} isLoading={isFetching && !currentItem} isError={isError} error={error} onPrevious={handlePrevious} onNext={handleNext} /> )} </div> ); }
2. 完善详情页组件(TodoDetails)
修正props引用错误,完善状态展示逻辑:
function TodoDetails({ item, isLoading, isError, error, onPrevious, onNext }) { if (isLoading) { return <div>Loading...</div>; } if (isError) { return <div>Error: {error?.message}</div>; } return ( <div style={{ marginTop: '20px', padding: '10px', border: '1px solid #eee' }}> <h2>{item.name}</h2> <p>{item.description}</p> <button onClick={onPrevious} disabled={!item} > Previous </button>{' '} <button onClick={onNext} disabled={!item} > Next </button> </div> ); }
关键逻辑说明
- 缓存数据读取:通过
queryClient.getQueriesData获取所有已加载的分页项目,合并为完整列表 - 索引计算:根据当前项目ID定位其在总列表中的位置,判断目标项是否存在于缓存
- 自动加载:目标项不在缓存时,通过
Math.floor(nextIndex / 9)计算对应页码,用prefetchQuery预加载页面,完成后切换到目标项 - 状态同步:切换到新页面项目时,同步更新当前分页,保持列表与详情页的页码一致性
内容的提问来源于stack exchange,提问作者Stuck
相关产品推荐
相关产品推荐

