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

React函数组件onScroll事件 setState异常:列表A滚动触发失效求助

问题描述

在自定义React函数组件的onScroll事件中设置变量状态时遇到异常:滚动列表B(内联编写的组件)时,控制台日志和scroll事件触发符合预期;但滚动列表A(提取为独立函数组件MyList)时,滚动框异常,需多次尝试才能触发onScroll的处理函数。最小复现代码如下:

import React, { useEffect, useState, useMemo, useCallback } from "react";
import _ from "lodash";
import ReactDOM from "react-dom";

export function App() {
  const [lastScrollTime, setLastScrollTime] = useState(0);
  const [nodeFound, setNodeFound] = useState(false);

  const ref = useCallback((node) => {
    if (node !== null) {
      console.log("node found");
      handleNodeRender();
    }
  }, []);

  const handleEndScroll = useMemo(
    () =>
      _.debounce(() => {
        console.log("stop");
        setLastScrollTime(0);
      }, 5000),
    []
  );

  const handleNodeRender = useMemo(
    () =>
      _.debounce(() => {
        console.log("render");
        setNodeFound(true);
      }, 100),
    []
  );
  useEffect(() => {
    const handleScroll = () => {
      if (lastScrollTime === 0) {
        console.log("start");
        setLastScrollTime(Date.now());
      }
      console.log("Scrolling");
      handleEndScroll();
    };
    console.log(document.querySelector("#scroll-0"));
    console.log(document.querySelector("#scroll-1"));
    if (nodeFound) {
      console.log("added listener");
      document
        .querySelector("#scroll-0")
        .addEventListener("scroll", handleScroll);
      document
        .querySelector("#scroll-1")
        .addEventListener("scroll", handleScroll);
    }
    return () => {
      if (nodeFound) {
        console.log("removed listener");
        document
          .querySelector("#scroll-0")
          .addEventListener("scroll", handleScroll);
        document
          .querySelector("#scroll-1")
          .removeEventListener("scroll", handleScroll);
      }
    };
  }, [lastScrollTime, nodeFound]);

  useEffect(() => {
    if (lastScrollTime === 0) {
      console.log("5 seconds waited");
    }
  }, [lastScrollTime]);

  const MyList = () => {
    return (
      <li>
        <div>
          <hr />
          <p>A</p>
          <div
            ref={ref}
            id="scroll-0"
            style={{ overflow: "auto", maxHeight: "70px" }}
          >
            <li>111</li>
            <li>222</li>
            <li>333</li>
            <li>444</li>
            <li>555</li>
            <li>666</li>
          </div>
        </div>
      </li>
    );
  };

  return (
    <div>
      <ul>
        <MyList />
        <li>
          <div>
            <hr />
            <p>B</p>
            <div
              ref={ref}
              id="scroll-1"
              style={{ overflow: "auto", maxHeight: "70px" }}
            >
              <li>111</li>
              <li>222</li>
              <li>333</li>
              <li>444</li>
              <li>555</li>
              <li>666</li>
            </div>
          </div>
        </li>
      </ul>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
问题原因
  1. 内部组件重复定义:MyList定义在App组件内部,每次App状态更新(如lastScrollTime、nodeFound变化)时,MyList会被重新定义为全新组件类型,React会卸载旧实例并挂载新实例,导致scroll-0元素频繁销毁重建,绑定的scroll事件失效。
  2. 清理函数错误:useEffect清理函数中对#scroll-0执行addEventListener而非removeEventListener,事件监听未被正确移除,引发逻辑混乱。
  3. 防抖延迟导致状态更新不及时:handleNodeRender防抖函数延迟设置nodeFound,使得元素挂载后事件监听绑定时机滞后。
解决方案

1. 迁移内部组件到外部

将MyList移到App组件外部,确保组件仅被定义一次,避免因App重渲染导致的组件卸载重建:

// 移到App组件外部
const MyList = ({ scrollRef }) => {
  return (
    <li>
      <div>
        <hr />
        <p>A</p>
        <div
          ref={scrollRef}
          id="scroll-0"
          style={{ overflow: "auto", maxHeight: "70px" }}
        >
          <li>111</li>
          <li>222</li>
          <li>333</li>
          <li>444</li>
          <li>555</li>
          <li>666</li>
        </div>
      </div>
    </li>
  );
};

export function App() {
  // ... 原有逻辑
}

2. 修复清理函数的事件移除错误

修正useEffect清理函数的错误操作,确保事件监听被正确移除:

useEffect(() => {
  const handleScroll = () => {
    // ... 原有逻辑
  };

  if (nodeFound) {
    console.log("added listener");
    document.querySelector("#scroll-0").addEventListener("scroll", handleScroll);
    document.querySelector("#scroll-1").addEventListener("scroll", handleScroll);
  }

  return () => {
    if (nodeFound) {
      console.log("removed listener");
      // 修正:add改为remove
      document.querySelector("#scroll-0").removeEventListener("scroll", handleScroll);
      document.querySelector("#scroll-1").removeEventListener("scroll", handleScroll);
    }
  };
}, [lastScrollTime, nodeFound]);

3. 优化ref状态更新逻辑

移除handleNodeRender防抖函数,直接在ref回调中设置nodeFound,确保元素挂载后立即绑定事件:

const ref = useCallback((node) => {
  if (node !== null) {
    console.log("node found");
    setNodeFound(true);
  }
}, []);

4. 稳定handleScroll引用

用useCallback包裹handleScroll,避免因依赖变化导致不必要的事件解绑重绑:

const handleScroll = useCallback(() => {
  if (lastScrollTime === 0) {
    console.log("start");
    setLastScrollTime(Date.now());
  }
  console.log("Scrolling");
  handleEndScroll();
}, [lastScrollTime, handleEndScroll]);

useEffect(() => {
  if (nodeFound) {
    console.log("added listener");
    document.querySelector("#scroll-0").addEventListener("scroll", handleScroll);
    document.querySelector("#scroll-1").addEventListener("scroll", handleScroll);
  }

  return () => {
    if (nodeFound) {
      console.log("removed listener");
      document.querySelector("#scroll-0").removeEventListener("scroll", handleScroll);
      document.querySelector("#scroll-1").removeEventListener("scroll", handleScroll);
    }
  };
}, [nodeFound, handleScroll]);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 02:20:26