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

React+Mongoose无限滚动重复取数问题及Range查询替代方案咨询

问题描述

我未使用任何NPM包实现了无限滚动功能,前端运行正常,但后端MongoDB会重复获取相同数据,该如何解决?另外,我了解到skip+limit不适用于大数据集,想知道如何使用range查询替代它。

相关代码

Next.js服务层

export const getMobilePosts = async (skip: any) => {
  endpoint = config.post.getMobilePosts + `?skip=${skip}`;
  return Axios.get(endpoint);
}

前端数据处理逻辑

try {
  const { data } = await getMobilePosts(skip);
  const filterData = await data?.filter(obj => obj.adPromote == "free-ad" || obj.adPromote == "reach-up-ad")
      .sort((f,p)=>{
          if (f.adPromote === 'reach-up-ad' && p.adPromote !== 'reach-up-ad') {
              return -1;
            } else if (f.adPromote !== 'reach-up-ad' && f.adPromote === 'reach-up-ad') {
              return 1;
            }
          return 0;
      })
      .map(obj => {
          return {
              ...obj,
              title: `${obj.title} (${obj.features.condition})`
          }
      });
  setPost([...post, ...filterData]);
  setSkip(filterData.length);
} catch (err) {
    console.log(err)
}

后端Node+Express服务

export async function getMobilePosts(skips: any) {
    try{
        const skip = skips ? Number(skips) : 0;
        const PER_REQUEST = 5;
    
        return await Post.find({expiredAt: {$gte: currentDate},status: 'live'},
                                "document fields goes here")
                                .sort({_id: -1 })
                                .skip(skip)
                                .limit(PER_REQUEST)

    } catch(error: any) {
        throw new Error(error as any);
    }
}

结果截图

结果截图1
结果截图2


解决方案

一、解决重复获取数据的问题

问题根源

前端的skip更新逻辑错误:每次请求后把skip设为当前返回的filterData.length,而非累计已加载的总条数。比如第一次请求skip=0返回5条,过滤后设skip=5;第二次请求skip=5返回5条,又把skip设为5,第三次请求依然用skip=5,自然会重复获取相同数据。

修复步骤

  1. 修正skip更新逻辑:用函数式更新累加已加载条数,确保每次请求的skip是累计值:
// 替换原setSkip(filterData.length)
setSkip(prevSkip => prevSkip + filterData.length);
  1. 增加无更多数据判断:当filterData.length === 0时,禁用滚动加载,避免无效请求:
if (filterData.length === 0) {
  setHasMore(false);
  return;
}
  1. 添加loading状态:防止同一时刻多次触发请求导致重复数据:
const [loading, setLoading] = useState(false);

const loadMore = async () => {
  if (loading) return;
  setLoading(true);
  try {
    // 原请求逻辑
  } finally {
    setLoading(false);
  }
}

二、用Range查询替代skip+limit(大数据集优化)

为什么skip+limit不适合大数据集?

MongoDB执行skip(n)时,需要先扫描掉前n条文档才能返回结果,当n很大时,扫描开销急剧上升,性能极差。而Range查询利用索引直接定位目标文档,效率高得多。

改造方案(基于_id字段,天生有序且唯一)

1. Next.js服务层:传递最后一条数据的_id替代skip

export const getMobilePosts = async (lastId: string | null) => {
  let endpoint = config.post.getMobilePosts;
  if (lastId) {
    endpoint += `?lastId=${lastId}`;
  }
  return Axios.get(endpoint);
}

2. 前端数据处理逻辑:跟踪最后一条数据的_id

// 初始化状态
const [post, setPost] = useState([]);
const [lastId, setLastId] = useState<string | null>(null);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);

const loadMore = async () => {
  if (!hasMore || loading) return;
  setLoading(true);
  try {
    const { data } = await getMobilePosts(lastId);
    const filterData = data?.filter(obj => obj.adPromote == "free-ad" || obj.adPromote == "reach-up-ad")
      .sort((f,p)=>{
        if (f.adPromote === 'reach-up-ad' && p.adPromote !== 'reach-up-ad') {
          return -1;
        } else if (f.adPromote !== 'reach-up-ad' && p.adPromote === 'reach-up-ad') {
          return 1;
        }
        return 0;
      })
      .map(obj => ({
        ...obj,
        title: `${obj.title} (${obj.features.condition})`
      }));
    
    if (filterData.length === 0) {
      setHasMore(false);
    } else {
      setPost(prev => [...prev, ...filterData]);
      // 更新lastId为当前最后一条数据的_id
      setLastId(filterData[filterData.length - 1]._id);
    }
  } catch (err) {
    console.log(err);
  } finally {
    setLoading(false);
  }
}

注:原sort逻辑有笔误,else if里的条件已修正为判断p.adPromote === 'reach-up-ad'。

3. 后端Node+Express服务:用Range查询替代skip

export async function getMobilePosts(req: Request) {
  try{
    const { lastId } = req.query;
    const PER_REQUEST = 5;
    const query = {
      expiredAt: {$gte: currentDate},
      status: 'live'
    };

    // 有lastId时,查询比它小的文档(按_id倒序,下一页数据的_id都比当前最后一条小)
    if (lastId) {
      query._id = { $lt: lastId };
    }

    return await Post.find(query, "document fields goes here")
      .sort({_id: -1 })
      .limit(PER_REQUEST);

  } catch(error: any) {
    throw new Error(error as any);
  }
}

关键注意事项

  • 必须使用唯一且有序的字段作为Range查询条件,_id是最优选择(MongoDB默认生成,包含时间戳,天生有序唯一);如果用createdAt,要确保它唯一,或结合_id一起排序。
  • 动态插入新文档时,倒序查询不会影响分页正确性,新文档只会出现在第一页。

内容的提问来源于stack exchange,提问作者asela daskon

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 16:15:09