React Instagram Reels克隆:scrollIntoView()与当前视频索引处理问题
Instagram Reels克隆项目滚动问题排查与修复
项目需求
通过链接参数获取1个视频,再获取4个随机视频;当上下相邻Reel进入视口时切换当前视频索引、更新URL;视频数组耗尽时加载新视频(当前处于静态5个视频测试阶段)。
核心问题
- 索引错乱:从第0个视频向下滚动到第1个来回正常,但滚动到第2、3个视频后向上滚动时,上下滚动事件会同时触发,导致
currentShotIndex同时增减(如出现2→3和2→1的并行变更),最终索引值混乱。 - 自动滚动失效:仅页面首次加载和从第0个视频滚动到第1个视频时,能在视频进入视口50%时自动滚动到组件中部,其余场景该功能完全失效。
相关代码
组件代码
function ViewShot(props) { const params = useParams(); const history = useHistory(); const [refNext, isNextIntersecting] = useOnScreen({ threshold: 0.5 }) const [prevRef, isPrevIntersecting] = useOnScreen({ threshold: 0.5 }) const [shots, setShots] = React.useState([]); const containerRef = React.useRef(null) const [wasLowerScroll, setWasLowerScroll] = React.useState(false) const [wasUpperScroll, setWasUpperScroll] = React.useState(false) const [firstLoad, setFirstLoad] = React.useState(true) const [currentShotIndex, setCurrentShotIndex] = React.useState(0); const [copySuccess, setCopySuccess] = React.useState(false); const querybyid = useGetShotByIdQuery(params.id); const queryrandom = useGetRandomShotsQuery(4); const { refetch: refetchShotByParams, data: shotbyid, error, isLoading } = querybyid const { refetch: refetchrandom, data: randomshots, error: randomerror, isLoading: randomLoad } = queryrandom React.useEffect(() => { if (querybyid.status == 'fulfilled' && queryrandom.status == 'fulfilled') { console.log('first useeffect') setShots([shotbyid, ...randomshots]); } }, [querybyid, queryrandom]); React.useEffect(() => { const fetchshot = async () => { if (isNextIntersecting && refNext.current) { const nextShotIndex = currentShotIndex + 1; console.log("scroll down - ", nextShotIndex) if (nextShotIndex < shots.length) { setCurrentShotIndex(nextShotIndex); if (shots) { history.push(`/shots/${shots[nextShotIndex]?._id}`); } } else { await refetchrandom(); setCurrentShotIndex(nextShotIndex); console.log("fetching new random shots"); if (shots) history.push(`/shots/${shots[nextShotIndex]?._id}`); } scrollToDiv(refNext) } } fetchshot(); }, [isNextIntersecting, refNext]); React.useEffect(() => { if (isPrevIntersecting && !firstLoad && prevRef.current) { const prevShotIndex = currentShotIndex - 1; if (prevShotIndex >= 0) { console.log("scroll up - ", prevShotIndex) setCurrentShotIndex(prevShotIndex); history.push(`/shots/${shots[prevShotIndex]?._id}`); scrollToDiv(prevRef) } } setFirstLoad(false) }, [isPrevIntersecting, prevRef]); const scrollToDiv = (element) => { if (element){ console.log(element.current) element.current.scrollIntoView() } } return ( <div style={{ ...container, ...custom }} className={`bg-light-color my-[100px]`}> <div className="p-[15px] h-[100%] flex"> <Menuleft /> <div style={{ flex: '1 85%', ...sliderContainer }} className="h-[100%] px-4" ref={containerRef}> {shots.length > 0 ? ( <> {shots.map((shot, index) => { return ( <div copyref={index === currentShotIndex + 1 ? "next" : (index === currentShotIndex - 1) ? "prevRef" : null} key={index} style={shotdiv} ref={index === currentShotIndex + 1 ? refNext : (index === currentShotIndex - 1) ? prevRef : null}> {shot && ( <div className="h-[100%]"> <ShotPlayer style={player} videoId={shot.video} /> </div> )} <div style={iconContainerStyle}> <div className="shadow-lg cursor-pointer" style={iconStyle}> <img className='h-[100%] p-1' src={like} alt="Like" /> <div style={text}>{formatViews(shot?.likes)}</div> </div> <div className="shadow-lg cursor-pointer" style={iconStyle}> <img className='h-[100%] p-1' src={dislike} alt="Dislike" /> <span style={text}>{formatViews(shot?.dislikes)}</span> </div> <div className="shadow-lg cursor-pointer" style={iconStyle}> <img className='h-[100%] p-2' src={comment} alt="Comment" /> <span style={text}>{formatViews(shot?.comments?.length)}</span> </div> <div className="shadow-lg cursor-pointer" style={iconStyle} onClick={handleCopy} > <img className='h-[100%] p-2' src={share} alt="Share" /> </div> <div className="shadow-lg cursor-pointer" style={iconStyle} onClick={() => history.push('/film?q=' + shot?.linksToOriginal[0])}> <div className='h-[100%] p-1 font-serif font-bold flex items-center justify-center'> Full </div> </div> </div> </div> ) })} </> ) : ( <> <h1>Nothing to show here</h1> </> )} {copySuccess && <span className="flex justify-center align-middle">URL copied to clipboard!</span>} </div> </div> </div> ) }
自定义useOnScreen Hook代码
import { useState, useEffect, useRef } from "react"; const useOnScreen = (options) => { const ref = useRef(); const [isIntersecting, setIntersecting] = useState(false); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => setIntersecting(entry.isIntersecting), options ); if (ref.current) { observer.observe(ref.current); } return () => { if (ref.current) { observer.unobserve(ref.current); } }; }, [ref, options]); return [ref, isIntersecting]; }; export default useOnScreen;
解决方案
1. 修复索引错乱问题
问题根源:滚动到第2、3个视频时,prevRef和refNext对应的元素可能同时进入容器视口,触发交叉检测,导致两个useEffect并行执行,同时修改currentShotIndex。
修复步骤:
- 增加
isScrolling状态标记滚动处理中,防止并发修改索引 - 合并滚动逻辑到单个
useEffect,优先处理单一方向的滚动事件 - 为交叉检测指定容器为根元素,缩小检测范围
修改后的核心逻辑:
const [isScrolling, setIsScrolling] = useState(false); React.useEffect(() => { if (isScrolling) return; const handleScroll = async () => { setIsScrolling(true); // 优先处理向下滚动 if (isNextIntersecting && refNext.current) { const nextShotIndex = currentShotIndex + 1; if (nextShotIndex < shots.length) { setCurrentShotIndex(nextShotIndex); history.push(`/shots/${shots[nextShotIndex]?._id}`); } else { // 等待新数据加载完成后再更新索引 const newRandomShots = await refetchrandom(); setShots(prev => [...prev, ...newRandomShots.data]); setCurrentShotIndex(nextShotIndex); history.push(`/shots/${newRandomShots.data[0]._id}`); } } // 再处理向上滚动 else if (isPrevIntersecting && !firstLoad && prevRef.current) { const prevShotIndex = currentShotIndex - 1; if (prevShotIndex >= 0) { setCurrentShotIndex(prevShotIndex); history.push(`/shots/${shots[prevShotIndex]?._id}`); } } // 500ms后解除锁定,避免短时间重复触发 setTimeout(() => setIsScrolling(false), 500); }; handleScroll(); }, [isNextIntersecting, isPrevIntersecting, refNext, prevRef, currentShotIndex, shots, history, firstLoad]);
同时修改useOnScreen的调用,指定根元素为容器:
const [refNext, isNextIntersecting] = useOnScreen({ threshold: 0.5, root: containerRef.current }) const [prevRef, isPrevIntersecting] = useOnScreen({ threshold: 0.5, root: containerRef.current })
2. 修复自动滚动失效问题
问题根源:currentShotIndex变化后,refNext和prevRef重新绑定元素,但交叉事件触发的滚动时机不对;且原scrollIntoView未指定滚动位置参数。
修复步骤:
- 存储所有视频元素的ref,当
currentShotIndex变化时,直接滚动到当前视频的中部 - 配置
scrollIntoView的block: 'center'和behavior: 'smooth'参数
修改后的代码:
// 存储所有视频元素的ref const shotRefs = useRef([]); // 新增useEffect,索引变化时自动滚动到当前视频 React.useEffect(() => { if (shots.length === 0 || currentShotIndex < 0 || currentShotIndex >= shots.length) return; const currentVideoRef = shotRefs.current[currentShotIndex]; if (currentVideoRef?.current) { currentVideoRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, [currentShotIndex, shots]); // 修改map逻辑,为每个视频绑定ref {shots.map((shot, index) => { shotRefs.current[index] = shotRefs.current[index] || useRef(null); return ( <div key={index} style={shotdiv} ref={shotRefs.current[index]} > {shot && ( <div className="h-[100%]"> <ShotPlayer style={player} videoId={shot.video} /> </div> )} {/* ... 其余图标容器代码 ... */} </div> ) })}
同时移除原交叉事件中的scrollToDiv调用,由索引变化的useEffect统一处理滚动逻辑。
内容的提问来源于stack exchange,提问作者Abilvap Emiramzaev
相关产品推荐
相关产品推荐

