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

React useEffect仅触发列表最后一个元素,求问题原因及解决方案

滚动动画仅最后一个子组件生效:问题分析与解决方案

问题根源

你遇到的问题本质是闭包陷阱结合自定义Hook的状态共享问题,导致所有子组件的滚动回调最终都指向最后一个组件的DOM引用:

  1. 若useScrollDirection Hook内部采用全局滚动监听+共享状态的实现方式,所有子组件会共用同一个滚动方向状态,触发更新时会批量执行所有组件的effect。
  2. 闭包特性会让effect捕获到组件最后一次渲染时的articleRef引用,最终只有最后一个组件的DOM会被修改。
  3. 父组件在useState中创建createRef并传递的方式,也可能因初始化逻辑的特性,导致ref引用被意外覆盖。

正确实现方案

方案1:修复useScrollDirection,保证组件状态隔离

如果useScrollDirection是自定义Hook,需确保每个组件实例拥有独立的滚动监听和状态:

// utils/hooks.js
import { useState, useEffect } from 'react';

export function useScrollDirection() {
  const [direction, setDirection] = useState(null);
  let lastScrollY = window.scrollY;

  useEffect(() => {
    const handleScroll = () => {
      const currentScrollY = window.scrollY;
      const newDirection = currentScrollY > lastScrollY ? 'DOWN' : 'UP';
      if (newDirection !== direction) {
        setDirection(newDirection);
      }
      lastScrollY = currentScrollY;
    };

    window.addEventListener('scroll', handleScroll, { passive: true });
    return () => window.removeEventListener('scroll', handleScroll);
  }, [direction]);

  return { direction };
}

方案2:子组件内部管理DOM引用(推荐)

将ref的创建移至子组件内部,彻底避免父组件传递ref带来的闭包问题:

子组件修改后代码

import React, { useEffect, useRef, useState } from "react";
import styles from "./article.module.css";
import { useScrollDirection, useImageToDeviceSize } from "utils/hooks";

const Article = ({ articleId }) => {
  const [canvasWidth, setCanvasWidth] = useState(null);
  const articleRef = useRef(null);
  const { width } = useImageToDeviceSize({
    initHeight: 400,
    initWidth: 400,
  });

  useEffect(() => {
    if (!width) return;
    setCanvasWidth(width);
  }, [width]);

  useEffect(() => {
    if (!canvasWidth) return;
    cloud(articleId);
  }, [canvasWidth]);

  const { direction } = useScrollDirection();

  const handleDirection = (direction) => {
    if (!articleRef.current) return;
    switch (direction) {
      case "DOWN":
        articleRef.current.style.cssText = "position: relative; left: 0; top: 0;";
        break;
      case "UP":
        articleRef.current.style.cssText = "position: relative; left: -32px; top: -32px;";
        break;
    }
  };

  useEffect(() => {
    if (!direction) return;
    handleDirection(direction);
  }, [direction]);

  return (
    <div className={styles.article} id={`div-${articleId}`} ref={articleRef}>
      {canvasWidth && <canvas width={canvasWidth} height={canvasWidth} id={articleId} />}
      {articleId}
    </div>
  );
};

export default Article;

父组件简化代码

import React from "react";
import styles from "/styles/Experiment.module.css";
import Article from "../components/Article";

const Experiment = () => {
  const articles = Array.from({ length: 3 }, (_, index) => ({
    articleId: index + 1
  }));

  return (
    <div className={styles.experiment}>
      {articles.map(({ articleId }) => (
        <Article key={articleId} articleId={articleId} />
      ))}
    </div>
  );
};

export default Experiment;

方案3:完善effect依赖项(兼容父传ref场景)

若必须保留父组件传递ref的方式,需将articleRef加入effect依赖数组,避免闭包捕获旧引用:

// 子组件中滚动相关的effect修改
useEffect(() => {
  if (!direction || !articleRef?.current) return;
  handleDirection(direction, articleRef);
}, [direction, articleRef]);

关键注意点

  • 自定义Hook必须保证状态和副作用的独立性,禁止全局共享状态引发组件间干扰。
  • 当effect中引用props、ref或变量时,务必将其加入依赖项数组,规避闭包陷阱。
  • 优先让子组件管理自身DOM引用,减少跨组件传递ref的复杂度。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 09:37:54