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

NextJS v15中useCenterColumn致scroll事件失效问题求助

问题:使用useCenterColumn钩子后滚动事件停止触发

我正在尝试追踪元素的水平滚动进度,未使用useCenterColumn钩子调整滚动父元素的子元素尺寸时一切正常。但使用该钩子后,父元素的scroll事件完全停止触发(包括ref?.addEventListener和div的onScroll)。我尝试过使用useLayoutEffect和useInsertionEffect,但无任何改善。需要解决:

  1. 如何确保sliderRef在useCenterColumn改变布局后仍能触发scroll事件?
  2. 这是否会在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 11:35:13