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; }
问题根源
- 事件监听切换的时序问题:当
indexScroll变为1时,移除纵向监听、添加横向监听的操作在useEffect中触发,但滚动事件可能在监听切换完成前就已触发,导致第一次滚动仍被纵向处理函数捕获,触发不必要的纵向滚动。 - 防抖函数的延迟干扰:防抖设置的50ms延迟,使得纵向处理函数可能在监听切换后仍会执行,引发残留的纵向滚动行为。
- 滚动定位参数不明确:
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]);
其他补充优化
- 改用标准的
deltaY判断滚动方向:替换原代码中的wheelDelta,deltaY是W3C标准属性,兼容性更好。 - 明确
scrollIntoView的定位参数:添加inline: "start"(横向)和block: "start"(纵向),确保滚动定位精准,避免默认行为导致的方向偏差。 - 简化条件判断逻辑:减少冗余代码,提升可读性。
内容的提问来源于stack exchange,提问作者Emiliano
相关产品推荐
相关产品推荐

