ReactJS与Mongoose无限滚动问题:置顶reach-up-ad及修复重复加载
问题修复与需求实现方案
1. 修复「无法将reach-up-ad置顶」的问题
当前前端仅对单批次加载的数据排序,导致新获取的reach-up-ad只能排在当前批次顶部,无法插入整个列表最前方。更可靠的方案是后端优先返回reach-up-ad数据,从根源保证排序逻辑:
修改Mongoose后端代码,调整排序规则,让reach-up-ad优先返回,再按创建时间倒序:
export async function getMobilePosts(skips: any) { try{ const skip = skips ? Number(skips) : 0; const DEFAULT_LIMIT = 10; return await Post.find({expiredAt: {$gte: currentDate},status: 'live'}, "features.subCategory features.condition swapType.type swapType.price createdAt features.mainCategory title location.district location.location photos adPromote status description postedBy notes") .skip(skip) .limit(DEFAULT_LIMIT) // 优先按adPromote排序,reach-up-ad前置,再按创建时间倒序 .sort({ $expr: { $cond: [{ $eq: ["$adPromote", "reach-up-ad"] }, 0, 1] }, _id: -1 }); } catch(error: any) { throw new Error(error as any); } }
前端可移除原有排序逻辑,简化代码:
const filterData = data?.filter(obj => obj.adPromote == "free-ad" || obj.adPromote == "reach-up-ad") .map(obj => { return { ...obj, title: `${obj.title} (${obj.features.condition})`, location: `Posted On ${new Date(obj.createdAt).toDateString()}` } });
2. 修复「点击Load more重复加载相同数据」的问题
当前用前端过滤后的posts.length作为分页skip值,因过滤会减少数据量,导致下一次请求的skip重复之前的范围。解决方案是维护独立的totalFetched状态,记录从后端请求过的总数据条数:
// 新增状态 const [totalFetched, setTotalFetched] = useState(0); const [isLoading, setIsLoading] = useState(false); // 防止并发请求 // 修改fetchPosts逻辑 const fetchPosts = async () => { if (isLoading) return; setIsLoading(true); try { const { data } = await getMobilePosts(totalFetched); // 用totalFetched作为skip参数 const filterData = data?.filter(obj => obj.adPromote == "free-ad" || obj.adPromote == "reach-up-ad") .map(obj => { return { ...obj, title: `${obj.title} (${obj.features.condition})`, location: `Posted On ${new Date(obj.createdAt).toDateString()}` } }); setPosts(existing => [...existing, ...filterData]); setTotalFetched(prev => prev + data.length); // 更新已请求的后端总条数 } catch (err) { console.error('加载失败:', err); } finally { setIsLoading(false); } }
3. 实现「移除Load more按钮,窗口滚动自动加载」
移除手动按钮,添加滚动监听,判断滚动到接近底部时自动触发加载:
// 初始加载 useEffect(() => { fetchPosts(); }, []); // 滚动监听逻辑 useEffect(() => { const handleScroll = () => { // 判断是否滚动到距离底部200px以内,且不在加载中 const isNearBottom = window.innerHeight + document.documentElement.scrollTop >= document.documentElement.offsetHeight - 200; if (isNearBottom && !isLoading) { fetchPosts(); } }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, [isLoading]); // 修改JSX,移除Load more按钮,添加加载提示 return ( <div> {posts?.map((data) => { return ( <ExchangeItemWideCard key={data._id} // 用数据ID作为key,避免索引导致的渲染问题 id={data._id} title={data.title} location={data.location} /> ) })} {isLoading && <div style={{textAlign:'center', padding:'1rem'}}>加载中...</div>} </div> )
完整前端代码整合
import { useState, useEffect } from 'react'; import ExchangeItemWideCard from './ExchangeItemWideCard'; const PostList = () => { const [posts, setPosts] = useState([]); const [totalFetched, setTotalFetched] = useState(0); const [isLoading, setIsLoading] = useState(false); const fetchPosts = async () => { if (isLoading) return; setIsLoading(true); try { const { data } = await getMobilePosts(totalFetched); if (!data || data.length === 0) return; const filterData = data.filter(obj => obj.adPromote === "free-ad" || obj.adPromote === "reach-up-ad" ).map(obj => ({ ...obj, title: `${obj.title} (${obj.features.condition})`, location: `Posted On ${new Date(obj.createdAt).toDateString()}` })); setPosts(existing => [...existing, ...filterData]); setTotalFetched(prev => prev + data.length); } catch (error) { console.error('加载帖子失败:', error); } finally { setIsLoading(false); } }; useEffect(() => { fetchPosts(); }, []); useEffect(() => { const handleScroll = () => { const isNearBottom = window.innerHeight + document.documentElement.scrollTop >= document.documentElement.offsetHeight - 200; if (isNearBottom && !isLoading) { fetchPosts(); } }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, [isLoading]); return ( <div> {posts.map((data) => ( <ExchangeItemWideCard key={data._id} id={data._id} title={data.title} location={data.location} /> ))} {isLoading && <div style={{ textAlign: 'center', padding: '1rem' }}>加载中...</div>} </div> ); }; export default PostList;
关键优化点总结
- 后端排序确保
reach-up-ad始终置顶,避免前端分批排序的局限性 - 用
totalFetched记录后端请求总量,彻底解决重复加载问题 - 滚动监听实现自动加载,移除手动按钮提升用户体验
- 增加
isLoading状态防止并发请求,用数据ID作为列表key提升渲染稳定性
内容的提问来源于stack exchange,提问作者asela daskon
相关产品推荐
相关产品推荐

