为什么状态未变化时自定义useInfiniteScroll钩子仍会重复渲染?
问题原因
- 核心是你调用
setIsAtBottom时没有做等值判断,即使新计算的底部状态和当前状态完全一致,也会触发状态更新流程。 - React官方渲染逻辑明确说明:当给状态设置与当前完全相同的值时,React会跳过子组件渲染与后续副作用执行,但可能会额外重渲染一次使用该状态的当前组件,这是React内部的校验逻辑导致的,仅作用于当前组件不会向下渗透,就是你步骤3看到的异常渲染来源。
- 后续再触发相同值的状态更新时,React会直接执行 bailout 逻辑完全跳过渲染,所以步骤4仅会打印状态日志不会打印RENDERED。
修复方案
只需在更新状态前对比新旧值,仅在值发生变化时执行状态更新即可,修改useInfiniteScroll的定时器回调逻辑:
import { useEffect, useRef, useState } from "react"; const isHitBottom = () => { const { scrollTop, scrollHeight, clientHeight } = document.documentElement; return scrollTop + clientHeight >= scrollHeight; }; export function useInfiniteScroll() { console.log("RENDERED"); const ref = useRef<NodeJS.Timeout | null>(null); const [isAtBottom, setIsAtBottom] = useState<boolean>(false); const debounced = () => { if (ref.current) { clearTimeout(ref.current); } ref.current = setTimeout(() => { const isBottom = isHitBottom(); // 用函数式更新拿到上一次状态做对比 setIsAtBottom(prevState => { if (isBottom === prevState) return prevState; console.log(isBottom ? "HIT BOTTOM" : "NOT HIT BOTTOM"); return isBottom; }); }, 500); }; useEffect(() => { window.addEventListener("scroll", debounced); return () => { window.removeEventListener("scroll", debounced); }; }, [debounced]); return isAtBottom; }
额外优化说明:给useEffect的依赖数组加上debounced,也可以配合useCallback包裹debounced函数,避免重复绑定滚动事件。
内容的提问来源于stack exchange,提问作者JunKim
相关产品推荐
相关产品推荐

