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

基于React-Query 4实现分页详情页的上下项切换及自动加载

实现TodoDetails组件的前后切换逻辑(支持自动加载分页)

现有基于React Query 4实现的分页项目列表(每页9项),需为TodoDetails组件实现以下功能:

  • 点击「Previous」「Next」按钮逐个切换项目
  • 切换到未加载页面的项目时,自动加载对应页面并展示目标项

核心思路

  1. 利用React Query的缓存机制,跟踪所有已加载的分页数据
  2. 在父组件(Todos)中维护当前选中项目的ID,统一管理分页与选中状态
  3. 切换时先从缓存查找目标项目,若不存在则计算对应页码并触发加载,完成后切换到目标项

代码实现

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 14:47:54