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

React组件中横向滚动前出现意外纵向滚动问题排查

React滚动模式切换时的异常纵向滚动问题修复

问题描述

开发React应用时,设置了基于组件状态的条件滚动逻辑:进入CurrentLocation组件(纵向索引为1)时切换为横向滚动,其他时候保持纵向滚动。但实际运行中,从纵向切换到横向滚动模式时,第一次横向滚动前会出现不必要的小幅纵向滚动,不符合预期效果。

期望效果:
期望滚动效果

实际效果:
实际滚动效果

相关代码

App.tsx

import "./App.css";
import CurrentLocation from "./components/CurrentLocation";
import Footer from "./components/Footer.js";
import Landing from "./components/Landing";
import { debounce } from "./utils/debounce.js";
import { useState, useEffect } from "react";

function App() {
  const [indexScroll, setIndexScroll] = useState(0);
  const [indexHoriz, setIndexHoriz] = useState(0);

  useEffect(() => {
    console.log("Current vertical index: ", indexScroll);

    // handle vertical scroll
    const handleScrolling = debounce((e: any) => {
      // get main element
      const main = document.querySelector("main");
      e.preventDefault();
      // if scrolling up
      if (e.wheelDelta <= 0) {
        // if we are not at the last div, scroll to next one
        if (indexScroll < main?.children.length!! - 1) {
          console.log("Scrolling up");
          setIndexScroll(indexScroll + 1);
          main?.children[indexScroll + 1].scrollIntoView({
            behavior: "smooth",
          });
        } else {
          // if last div, go back to the first one
          setIndexScroll(0);
          main?.children[0].scrollIntoView({
            behavior: "smooth",
          });
        }
      } else {
        // if scrolling down
        // if we are not in the first div, scroll back
        if (indexScroll > 0) {
          setIndexScroll(indexScroll - 1);
          main?.children[indexScroll - 1].scrollIntoView({
            behavior: "smooth",
          });
        } else {
          // if we are at the first div, do nothing
          console.log("You are at the first div, can't scroll back.");
        }
      }
    }, 50);

    // handle horiz scroll
    const handleScrollingHoriz = debounce((e: any) => {
      // get main element
      const main = document.querySelector("#current-location");
      e.preventDefault();
      // if scrolling up
      if (e.wheelDelta <= 0) {
        // if we are not at the last div, scroll to next one
        if (indexHoriz < main?.children.length!! - 1) {
          console.log("Scrolling up");
          setIndexHoriz(indexHoriz + 1);
          main?.children[indexHoriz + 1].scrollIntoView({
            behavior: "smooth",
          });
        } else {
          // if last div, go back to the first one
          setIndexHoriz(0);
          main?.children[0].scrollIntoView({
            behavior: "smooth",
          });
        }
      } else {
        // if scrolling down
        // if we are not in the first div, scroll back
        if (indexHoriz > 0) {
          setIndexHoriz(indexHoriz - 1);
          main?.children[indexHoriz - 1].scrollIntoView({
            behavior: "smooth",
          });
        } else {
          // if we are at the first div, do nothing
          console.log("You are at the first div, can't scroll back.");
        }
      }
    }, 50);

    // if user is in div n°1 (current location) activate horizontal scrolling
    if (indexScroll === 1) {
      window.removeEventListener("mousewheel", handleScrolling);
      window.addEventListener("mousewheel", handleScrollingHoriz);
    } else {
      window.addEventListener("mousewheel", handleScrolling);
    }
  }, [indexScroll, indexHoriz]);

  return (
    <main>
      <Landing />
      <CurrentLocation />
      <Footer />
    </main>
  );
}

export default App;

CurrentLocation组件

import useGeo from "../hooks/useGeo";

const CurrentLocation = () => {
  const { geoData, weatherData, geoLoading, weatherLoading } = useGeo();

  if (geoLoading || weatherLoading)
    return (
      <div id="current-location" className="grid">
        <p>Loading data</p>
      </div>
    );

  return (
    <div id="current-location" className="grid">
      <div className="grid-child">
        <h1>
          It looks like you're in {geoData.city}, {geoData.state_prov}.
          <br /> The current weather is{" "}
          {weatherData.list[0].weather[0].description} with a temperature of{" "}
          {weatherData.list[0].main.temp.toFixed(0)}°F.
        </h1>
      </div>
      <div className="grid-child">FORECAST 5 DAYS</div>
      <div className="grid-child">SEARCH LOCATION</div>
    </div>
  );
};

export default CurrentLocation;

App.css

.full-page > div {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 90%;
  width: 90%;
}

.full-page {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100dvh;
}

#current-location,
.grid-child {
  overflow-x: hidden;
}

.grid {
  display: grid;
  height: 100dvh;
  align-items: center;
  grid-template-columns: 100vw 100vw 100vw;
}

.grid-child {
  display: flex;
  justify-content: center;
  width: 100vw;
}

index.css

body {
  box-sizing: border-box;
  background-color: black;
  color: white;
  overflow: hidden;
  font-family: "DM Sans", sans-serif;
}

h1 {
  padding: 30px;
  font-size: 2.5em;
  font-weight: 400;
}

问题根源

  1. 事件监听切换的时序问题:当indexScroll变为1时,移除纵向监听、添加横向监听的操作在useEffect中触发,但滚动事件可能在监听切换完成前就已触发,导致第一次滚动仍被纵向处理函数捕获,触发不必要的纵向滚动。
  2. 防抖函数的延迟干扰:防抖设置的50ms延迟,使得纵向处理函数可能在监听切换后仍会执行,引发残留的纵向滚动行为。
  3. 滚动定位参数不明确:scrollIntoView未指定明确的定位参数,默认行为可能导致滚动方向的偏差。

修复方案

核心优化:合并滚动处理函数,避免频繁切换监听

将纵向和横向滚动逻辑合并到同一个处理函数中,根据当前indexScroll的值判断执行哪种滚动逻辑,只绑定一次全局滚动监听,彻底解决时序问题。

修改后的App.tsx中useEffect部分代码:

useEffect(() => {
  const handleScroll = debounce((e: WheelEvent) => {
    e.preventDefault();
    
    if (indexScroll === 1) {
      // 横向滚动逻辑
      const container = document.querySelector("#current-location");
      if (!container) return;
      const maxIndex = container.children.length - 1;

      if (e.deltaY > 0) {
        // 滚轮向下 → 横向向右滚动
        const newIndex = indexHoriz >= maxIndex ? 0 : indexHoriz + 1;
        setIndexHoriz(newIndex);
        container.children[newIndex].scrollIntoView({ 
          behavior: "smooth",
          inline: "start" // 明确横向对齐方式
        });
      } else {
        // 滚轮向上 → 横向向左滚动
        const newIndex = indexHoriz <= 0 ? maxIndex : indexHoriz - 1;
        setIndexHoriz(newIndex);
        container.children[newIndex].scrollIntoView({ 
          behavior: "smooth",
          inline: "start"
        });
      }
    } else {
      // 纵向滚动逻辑
      const container = document.querySelector("main");
      if (!container) return;
      const maxIndex = container.children.length - 1;

      if (e.deltaY > 0) {
        // 滚轮向下 → 纵向向下滚动
        const newIndex = indexScroll >= maxIndex ? 0 : indexScroll + 1;
        setIndexScroll(newIndex);
        container.children[newIndex].scrollIntoView({ 
          behavior: "smooth",
          block: "start" // 明确纵向对齐方式
        });
      } else {
        // 滚轮向上 → 纵向向上滚动
        if (indexScroll > 0) {
          const newIndex = indexScroll - 1;
          setIndexScroll(newIndex);
          container.children[newIndex].scrollIntoView({ 
            behavior: "smooth",
            block: "start"
          });
        }
      }
    }
  }, 50);

  window.addEventListener("mousewheel", handleScroll);
  // 组件卸载时移除监听
  return () => window.removeEventListener("mousewheel", handleScroll);
}, [indexScroll, indexHoriz]);

其他补充优化

  1. 改用标准的deltaY判断滚动方向:替换原代码中的wheelDelta,deltaY是W3C标准属性,兼容性更好。
  2. 明确scrollIntoView的定位参数:添加inline: "start"(横向)和block: "start"(纵向),确保滚动定位精准,避免默认行为导致的方向偏差。
  3. 简化条件判断逻辑:减少冗余代码,提升可读性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 10:57:07