NextJS v15中useCenterColumn致scroll事件失效问题求助
问题:使用useCenterColumn钩子后滚动事件停止触发
我正在尝试追踪元素的水平滚动进度,未使用useCenterColumn钩子调整滚动父元素的子元素尺寸时一切正常。但使用该钩子后,父元素的scroll事件完全停止触发(包括ref?.addEventListener和div的onScroll)。我尝试过使用useLayoutEffect和useInsertionEffect,但无任何改善。需要解决:
- 如何确保
sliderRef在useCenterColumn改变布局后仍能触发scroll事件? - 这是否会在NextJS v15中产生副作用?
import { FC, useEffect, useRef, useState } from "react"; import cx from "classnames"; import Link from "next/link"; import { BlockWrapper, Image } from "components"; import { useSliderMutate } from "contexts/SliderContext"; const Breakpoints: { [key: string]: number } = { xs: 0, xl: 1200, "3xl": 1920, }; const ContainerPadding: { [key: string]: number } = { xs: 29, xl: 60, "3xl": 96, }; const useCenterColumn = () => { const [centerColumn, setCenterColumn] = useState(0); useEffect(() => { const updateCenterColumn = () => { const vw = typeof window !== "undefined" ? Math.max(window?.innerWidth || 0) : 0; const breakpoints = Object.keys(Breakpoints); const breakpoint = breakpoints.find((key, i) => { if (i === breakpoints.length - 1) return vw >= Breakpoints[key]; return vw >= Breakpoints[key] && vw < Breakpoints[breakpoints[i + 1]]; }); if (!breakpoint) return 0; const padding = ContainerPadding[breakpoint]; const result = breakpoint === "3xl" ? Math.min(((vw - padding * 2) / 14) * 12 - 20, ((1920 - padding * 2) / 14) * 12 - 20) : ((vw - padding * 2) / 14) * 12 - 20; setCenterColumn(result); }; window.addEventListener("resize", updateCenterColumn); updateCenterColumn(); return () => window.removeEventListener("resize", updateCenterColumn); }, []); return centerColumn; }; const ImageSlider: FC<any> = ({ items, meta }) => { const sliderRef = useRef<HTMLDivElement | null>(null); const mutate = useSliderMutate(); const centerColumn = useCenterColumn(); useEffect(() => { const ref = sliderRef.current; const handleScroll = () => { const max = (ref?.scrollWidth || 0) - (ref?.offsetWidth || 0); const current = ref?.scrollLeft || 0; const position = (current / max) * 100 || 0; mutate?.setScrollPosition(meta.key, position); }; handleScroll(); window.addEventListener("resize", handleScroll, true); ref?.addEventListener("scroll", handleScroll, true); return () => { window.removeEventListener("resize", handleScroll); ref?.removeEventListener("scroll", handleScroll); }; }, [sliderRef, mutate, meta.key, centerColumn]); return ( <BlockWrapper layout="full" className="ImageSlider"> <div className="ImageSlider__container w-full max-w-full mx-auto overflow-auto _scroll-hide relative"> <div key={`Slider-Track--${meta.key}`} ref={sliderRef} onScroll={() => console.log('SCROLLL')} className="ImageSlider__track w-fit flex pl-7 md:pl-[calc((100vw-(28px*2))/14+28px)] lg:pl-[calc((100vw-(38px*2))/14+38px)] xl:pl-[calc((100vw-(60px*2))/14+60px)] 2xl:pl-[calc((100vw-(76px*2))/14+76px)] 3xl:pl-[calc(max(((100vw-(96px*2))/14)+96px,(100vw-(1728px/14*12))/2))] relative overflow-auto _scroll-hide" > {items.map((item) => { return ( <Link key={item._key} href={item.url || ""} className={cx( "relative flex-shrink-0 block mr-5 lg:mr-[26px] xl:mr-8 2xl:mr-10 3xl:mr-[52px] md:last:mr-[calc((100vw-(28px*2))/14+28px)] lg:last:mr-[calc((100vw-(38px*2))/14+38px)] xl:last:mr-[calc((100vw-(60px*2))/14+60px)] 2xl:last:mr-[calc((100vw-(76px*2))/14+76px)] 3xl:last:mr-[calc(max(((100vw-(96px*2))/14)+96px,(100vw-(1728px/14*12))/2))]", {}, )} style={{ width: (centerColumn / 6) * 5 - 20, }} > <Image src="" alt="" /> </Link> ); })} </div> </div> </BlockWrapper> ); }; export default ImageSlider;
若移除useCenterColumn的使用,scroll事件恢复正常,但item元素尺寸会显示错误。
问题原因
核心问题是centerColumn更新触发组件重渲染时,事件绑定的时机和依赖项配置错误,加上双层滚动容器的CSS设置,导致滚动事件丢失或未被正确监听。
修复方案
1. 修正useEffect依赖项
useRef是稳定引用,无需加入依赖数组,移除后避免不必要的事件解绑/重建:
useEffect(() => { const ref = sliderRef.current; if (!ref) return; const handleScroll = () => { const max = ref.scrollWidth - ref.offsetWidth; const current = ref.scrollLeft; // 避免除以0的情况 const position = max > 0 ? (current / max) * 100 : 0; mutate?.setScrollPosition(meta.key, position); }; handleScroll(); ref.addEventListener("scroll", handleScroll); window.addEventListener("resize", handleScroll); return () => { ref.removeEventListener("scroll", handleScroll); window.removeEventListener("resize", handleScroll); }; }, [mutate, meta.key, centerColumn]); // 移除sliderRef依赖
2. 合并滚动容器
当前代码有两层overflow-auto的容器,导致实际滚动的是上层ImageSlider__container,而非你监听的sliderRef所在的下层容器。移除下层容器的overflow-auto类:
<div key={`Slider-Track--${meta.key}`} ref={sliderRef} onScroll={() => console.log('SCROLLL')} className="ImageSlider__track w-fit flex pl-7 md:pl-[calc((100vw-(28px*2))/14+28px)] lg:pl-[calc((100vw-(38px*2))/14+38px)] xl:pl-[calc((100vw-(60px*2))/14+60px)] 2xl:pl-[calc((100vw-(76px*2))/14+76px)] 3xl:pl-[calc(max(((100vw-(96px*2))/14)+96px,(100vw-(1728px/14*12))/2))] relative _scroll-hide" >
3. 用useLayoutEffect确保DOM更新后绑定事件
如果布局更新后事件仍未触发,换成useLayoutEffect,确保在DOM布局计算完成后立即绑定事件:
import { FC, useLayoutEffect, useRef, useState } from "react"; // ... useLayoutEffect(() => { const ref = sliderRef.current; if (!ref) return; const handleScroll = () => { const max = ref.scrollWidth - ref.offsetWidth; const current = ref.scrollLeft; const position = max > 0 ? (current / max) * 100 : 0; mutate?.setScrollPosition(meta.key, position); }; handleScroll(); ref.addEventListener("scroll", handleScroll); window.addEventListener("resize", handleScroll); return () => { ref.removeEventListener("scroll", handleScroll); window.removeEventListener("resize", handleScroll); }; }, [mutate, meta.key, centerColumn]);
Next.js v15副作用说明
你的代码使用的都是标准React API,在Next.js v15中不会有兼容性问题:
- 服务端渲染时,
useCenterColumn中已经判断了window对象的存在,不会触发SSR报错; - React 18/19的生命周期逻辑和之前一致,事件绑定/解绑的逻辑完全兼容;
- 只要确保事件绑定在客户端执行(
useEffect/useLayoutEffect在SSR阶段不会运行),就不会有 hydration 相关的问题。
内容的提问来源于stack exchange,提问作者JLF
相关产品推荐
相关产品推荐

