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

React(NextJS)中window滚动事件无法获取最新状态问题求解

问题根源

这是React Hooks典型的闭包陈旧值问题:空依赖数组的useEffect仅在组件首次挂载时执行一次,此时绑定到window上的scroll事件回调,会被闭包锁定在首次渲染的上下文里,回调内部引用的router、isComplete、isFetching等所有状态,永远是组件挂载时的初始值,后续状态更新不会同步到已绑定的回调函数中。
如果直接移除空依赖数组、不做清理逻辑,会在每次相关状态更新时新增一个scroll监听器,旧监听器不会自动销毁,最终出现事件触发次数远超预期的问题。

解决方案

方案1:ref同步最新状态(性能优先推荐)

这个方案不需要频繁解绑/重绑滚动事件,通过ref挂载所有回调需要访问的动态值,让事件回调永远能读到最新状态,完全避开闭包问题,是滚动这类高频事件场景下的最优实现。

import { useEffect, useRef, useCallback } from 'react';
import { useRouter } from 'next/router';

// 组件内核心逻辑
const router = useRouter();
const infiniteScroll = useRef(null);
// 新建ref存储滚动回调需要用到的所有动态值
const latestStateRef = useRef({
  isComplete: false,
  isFetching: false,
  routerQuery: router.query
});

// 每次相关状态更新时,同步最新值到ref
useEffect(() => {
  latestStateRef.current = {
    isComplete,
    isFetching,
    routerQuery: router.query
  };
}, [isComplete, isFetching, router.query]);

const handleScroll = useCallback(() => {
  const el = infiniteScroll.current;
  if (!el) return;
  
  const rect = el.getBoundingClientRect();
  const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
  const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
  const isVisible = rect.top >= 0 && rect.left >= 0 && rect.bottom <= viewportHeight && rect.right <= viewportWidth;

  // 从ref中读取最新状态,不存在闭包陈旧问题
  const { isComplete: latestIsComplete, isFetching: latestIsFetching, routerQuery } = latestStateRef.current;
  if (isVisible && !latestIsComplete && !latestIsFetching) {
    const params = { ...routerQuery };
    params.page = params.page ? (parseInt(params.page) + 1).toString() : '1';
    router.replace({ query: params }, undefined, { scroll: false });
  }
}, [router]);

// 仅在组件挂载、卸载时绑定/解绑一次事件,不会重复绑定
useEffect(() => {
  window.addEventListener('scroll', handleScroll, { passive: true });
  return () => window.removeEventListener('scroll', handleScroll);
}, [handleScroll]);

优化提示:给scroll事件添加passive: true配置可以避免事件回调阻塞浏览器滚动渲染,大幅提升页面滚动流畅度;另外不要直接修改router.query这个只读对象,操作前先做浅拷贝,避免触发隐式异常。

方案2:useEffect全依赖+清理函数

如果不想引入额外ref,可以把事件回调定义在useEffect内部,把所有回调用到的状态加入依赖数组,同时在effect的返回函数里每次重绑前先移除旧监听器,保证同一时间只有一个生效的监听器。这个方案逻辑更直观,但因为每次状态变化都会重绑事件,高频滚动场景下性能略差于方案1。

useEffect(() => {
  const handleScroll = () => {
    const el = infiniteScroll.current;
    if (!el) return;
    
    const rect = el.getBoundingClientRect();
    const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
    const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
    const isVisible = rect.top >=0 && rect.left >=0 && rect.bottom <= viewportHeight && rect.right <= viewportWidth;

    if (isVisible && !isComplete && !isFetching) {
      const params = { ...router.query };
      params.page = params.page ? (parseInt(params.page) + 1).toString() : '1';
      router.replace({ query: params }, undefined, { scroll: false });
    }
  };

  window.addEventListener('scroll', handleScroll, { passive: true });
  // 每次effect重跑前、组件卸载时,移除上一次绑定的监听器
  return () => window.removeEventListener('scroll', handleScroll);
// 把所有回调内用到的外部值全部加入依赖数组
}, [isComplete, isFetching, router, infiniteScroll.current]);
常见避坑点
  • 不要试图通过在滚动回调里触发强制重渲染的方式拿最新值,会导致滚动过程中组件频繁重绘,引发页面卡顿
  • 不要用脱离React生命周期的全局变量存储状态,多组件实例共存时会出现状态串扰
  • 全窗口滚动场景不需要给容器设置固定高度加overflow-y: scroll,直接监听window的scroll事件就是符合需求的原生实现

内容的提问来源于stack exchange,提问作者Nina Horne

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 18:45:40