React useEffect仅触发列表最后一个元素,求问题原因及解决方案
滚动动画仅最后一个子组件生效:问题分析与解决方案
问题根源
你遇到的问题本质是闭包陷阱结合自定义Hook的状态共享问题,导致所有子组件的滚动回调最终都指向最后一个组件的DOM引用:
- 若
useScrollDirectionHook内部采用全局滚动监听+共享状态的实现方式,所有子组件会共用同一个滚动方向状态,触发更新时会批量执行所有组件的effect。 - 闭包特性会让effect捕获到组件最后一次渲染时的
articleRef引用,最终只有最后一个组件的DOM会被修改。 - 父组件在
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
相关产品推荐
相关产品推荐

