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

React Instagram Reels克隆:scrollIntoView()与当前视频索引处理问题

Instagram Reels克隆项目滚动问题排查与修复

项目需求

通过链接参数获取1个视频,再获取4个随机视频;当上下相邻Reel进入视口时切换当前视频索引、更新URL;视频数组耗尽时加载新视频(当前处于静态5个视频测试阶段)。

核心问题

  1. 索引错乱:从第0个视频向下滚动到第1个来回正常,但滚动到第2、3个视频后向上滚动时,上下滚动事件会同时触发,导致currentShotIndex同时增减(如出现2→3和2→1的并行变更),最终索引值混乱。
  2. 自动滚动失效:仅页面首次加载和从第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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 11:49:51