反向无限滚动加载时滚动位置偏移问题排查求助
反向无限滚动加载后滚动位置偏移问题排查
问题描述
实现反向无限滚动效果,当前滚动到顶部能触发数据加载,但加载新数据后滚动位置固定在顶部,预期加载后应保持在当前查看的内容位置。
服务端返回数据格式
[{ channelIdx: 1, sender: 'wochae', msg: 'channelIdx 1, msg 71' },{ channelIdx: 1, sender: 'jujeon', msg: 'channelIdx 1, msg 72' },{ channelIdx: 1, sender: 'jaekim', msg: 'channelIdx 1, msg 73' },{ channelIdx: 1, sender: 'haryu', msg: 'channelIdx 1, msg 74' },{ channelIdx: 1, sender: 'wochae', msg: 'channelIdx 1, msg 75' }]
现有代码
App.js
import Chats2 from "./Chats2"; const App = () => { return ( <div style={{ width: "50%", height: "500px", margin: "auto", backgroundColor: "purple", overflow: "auto", }} > <h1>Inverse Scroll</h1> <Chats2 /> </div> ); }; export default App;
Chats2.js
import axios from "axios"; import { useEffect, useState, useRef, useCallback } from "react"; const TOTAL_PAGES = 100; const options = { threshold: 0.1, }; const Chats = () => { const [loading, setLoading] = useState(true); const [pageNum, setPageNum] = useState(0); const [chats, setChats] = useState([]); const observerTarget = useRef(null); useEffect(() => { const observer = new IntersectionObserver( (entries) => { if (entries[0]?.isIntersecting) { setPageNum((num) => num + 1) } }, options) if (observerTarget.current) { observer.observe(observerTarget.current) } return () => { if (observerTarget.current) { observer.unobserve(observerTarget.current) } } }, [observerTarget]) const callUser = useCallback(async () => { console.log(pageNum); await axios .get(`http://localhost:4000/chat/messages?channelIdx=1&index=${pageNum}`) .then((res) => { const newData = Array.isArray(res.data) ? res.data : [res.data]; setChats((prevChats) => [...prevChats, ...newData]); setLoading(false); }); }, [pageNum]); useEffect(() => { if (pageNum <= TOTAL_PAGES) { setTimeout(() => { callUser(); }, 500); setLoading(true); } }, [pageNum]); return ( <> <div style={{ display: "flex", flexDirection: "column-reverse", overflow: "auto", overflowAnchor: "none", position: "sticky", overflowAnchor: "none", }} > {chats.map((chats, i) => { return ( <div key={i} style={{ height: "100px", }} > <li>{chats.sender}</li> <li>{chats.msg}</li> </div> ); })} <div ref={observerTarget}></div> {loading === true && <p>loading...</p>} </div> </> ); }; export default Chats;
问题原因分析
- 数据追加方向错误:使用
flex-direction: column-reverse时,数组末尾对应视觉顶部,当前代码把新数据追加到数组末尾,导致旧内容被挤到下方,滚动位置自然跳转到顶部。 - 滚动位置未做补偿:加载新数据后,容器总高度增加,没有计算并调整滚动偏移,无法维持原查看位置。
- 观察目标位置错误:IntersectionObserver的目标放在了视觉底部(数组渲染后的末尾),反向滚动时应该观察视觉顶部的元素,才能正确触发加载。
- 无效样式干扰:重复设置
overflowAnchor: none,且position: sticky无意义,可能影响滚动行为。
修复方案
1. 调整数据更新逻辑
将新数据添加到数组开头,对应视觉顶部的新内容:
setChats((prevChats) => [...newData, ...prevChats]);
2. 记录并补偿滚动位置
添加滚动容器的ref,加载前记录当前滚动状态,加载后计算新的滚动位置:
- 新增
scrollContainerRef = useRef(null) - 在请求前后记录并调整滚动偏移,抵消新数据带来的高度变化
3. 调整观察目标位置
将observerTarget放在渲染列表的最前面(视觉顶部),确保滚动到顶部时能触发加载检测。
4. 清理无效样式
去掉position: sticky,合并重复的overflowAnchor设置,避免样式冲突。
修改后的完整Chats2.js代码
import axios from "axios"; import { useEffect, useState, useRef, useCallback } from "react"; const TOTAL_PAGES = 100; const options = { threshold: 0.1, }; const Chats = () => { const [loading, setLoading] = useState(true); const [pageNum, setPageNum] = useState(0); const [chats, setChats] = useState([]); const observerTarget = useRef(null); const scrollContainerRef = useRef(null); useEffect(() => { const observer = new IntersectionObserver( (entries) => { if (entries[0]?.isIntersecting && !loading) { setPageNum((num) => num + 1); } }, options); if (observerTarget.current) { observer.observe(observerTarget.current); } return () => { if (observerTarget.current) { observer.unobserve(observerTarget.current); } }; }, [loading]); const callUser = useCallback(async () => { if (pageNum > TOTAL_PAGES) return; const scrollContainer = scrollContainerRef.current; if (!scrollContainer) return; // 记录加载前的滚动状态 const oldScrollHeight = scrollContainer.scrollHeight; const oldScrollTop = scrollContainer.scrollTop; try { const res = await axios.get(`http://localhost:4000/chat/messages?channelIdx=1&index=${pageNum}`); const newData = Array.isArray(res.data) ? res.data : [res.data]; setChats((prevChats) => [...newData, ...prevChats]); // 等待DOM更新后调整滚动位置 setTimeout(() => { const newScrollHeight = scrollContainer.scrollHeight; scrollContainer.scrollTop = newScrollHeight - oldScrollHeight + oldScrollTop; }, 0); setLoading(false); } catch (err) { console.error(err); setLoading(false); } }, [pageNum]); useEffect(() => { if (pageNum <= TOTAL_PAGES && !loading) { setLoading(true); setTimeout(() => { callUser(); }, 500); } }, [pageNum, loading]); return ( <> <div ref={scrollContainerRef} style={{ display: "flex", flexDirection: "column-reverse", overflow: "auto", overflowAnchor: "none", }} > <div ref={observerTarget} style={{ height: "1px" }}></div> {chats.map((chat, i) => { return ( <div key={i} style={{ height: "100px", }} > <li>{chat.sender}</li> <li>{chat.msg}</li> </div> ); })} {loading && <p>loading...</p>} </div> </> ); }; export default Chats;
内容的提问来源于stack exchange,提问作者jujeon
相关产品推荐
相关产品推荐

