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

React移动端底部导航栏滚动至顶部时无法正常显示求助

解决React移动端BottomBar滚动可见性不稳定问题

你的BottomBar组件在移动端滚动时显示异常,核心问题出在这几点:

  • 闭包与状态依赖错误:useEffect依赖scrollDirection状态,导致每次方向变化都会重新绑定scroll事件,且handleScroll里的scrollDirection是闭包捕获的旧值,判断可见性时逻辑滞后。
  • 异步状态更新导致逻辑不同步:scroll事件里连续调用setScrollDirection和setBottomBarVisible,状态更新异步特性导致判断条件拿不到最新值。
  • 移动端滚动特性未适配:移动端滚动有惯性,window.scrollY可能不会精确停在0,且scroll事件触发频率极高,容易导致状态更新混乱。

修复后的代码实现

import { useEffect, useState, useRef } from 'react';
import {
  BsHeartFill,
  BsFillPersonFill,
  BsFillBasket2Fill,
  BsCart2,
} from 'react-icons/bs';
import { Link, useLocation } from 'react-router-dom';
import { AiFillHome } from 'react-icons/ai';
import { IoIosCube } from 'react-icons/io';
import CategorySVG from './SVGS/CategorySVG';
import useUser from './hooks/useUser';
import { useDispatch } from 'react-redux';
import { success } from '../common/Common.Action';

export default function BottomBar({
  data,
  onMoveWhishListItem,
  handleClick,
  addCart,
  foundCartProduct,
  itemChecked,
  addProductToCart,
  handleCheckout,
  buyProduct,
}) {
  const [bottomBarVisible, setBottomBarVisible] = useState(true);
  const { isAuthenticated } = useUser();
  const location = useLocation();
  const dispatch = useDispatch();
  const prevScrollY = useRef(window.scrollY);
  const throttleTimeout = useRef(null);

  const productDetailsSlug = location.pathname.includes('product-details');

  useEffect(() => {
    const handleScroll = () => {
      // 节流处理,避免scroll事件频繁触发
      if (throttleTimeout.current) return;
      throttleTimeout.current = setTimeout(() => {
        const currentScrollY = window.scrollY;
        const isScrollingDown = currentScrollY > prevScrollY.current;
        
        // 可见性判断逻辑:
        // 1. 滚动到顶部(阈值放宽到<=10,适配移动端惯性)
        // 2. 向上滚动时显示
        // 3. 向下滚动时隐藏
        if (currentScrollY <= 10 || !isScrollingDown) {
          setBottomBarVisible(true);
        } else {
          setBottomBarVisible(false);
        }

        prevScrollY.current = currentScrollY;
        throttleTimeout.current = null;
      }, 100); // 100ms节流间隔,平衡响应速度和性能
    };

    // 移动端添加passive选项提升滚动性能
    window.addEventListener('scroll', handleScroll, { passive: true });

    return () => {
      window.removeEventListener('scroll', handleScroll);
      if (throttleTimeout.current) clearTimeout(throttleTimeout.current);
    };
  }, []); // 空依赖,只绑定一次事件

  return (
    <>
      {!productDetailsSlug ? (
        <div
          className={`bg-white h-16 z-20 text-gray-700 fixed bottom-0 left-0 right-0 max-w-screen md:hidden flex justify-center items-center ${
            !bottomBarVisible ? 'bottom-bar hidden' : ''
          }`}
        >
          <div className="flex justify-between items-center w-full px-4">
            <Link
              to="/"
              className={`flex flex-col items-center ${
                location.pathname === '/' ? 'text-[#ec6525]' : ''
              }`}
            >
              <AiFillHome size={25} />
              <p className="text-[12px] font-medium">Home</p>
            </Link>
            <Link
              className={`flex flex-col items-center ${
                location.pathname === '/all-category' ? 'text-[#ec6525]' : ''
              }`}
              to="/all-category"
            >
              <CategorySVG />
              <p className="text-[12px] font-medium">Categories</p>
            </Link>
            {!isAuthenticated ? (
              <Link
                to="/login"
                className={`flex flex-col items-center ${
                  location.pathname === '/login' ? 'text-[#ec6525]' : ''
                }`}
                state={{ prev: '/' }}
              >
                <BsFillPersonFill size={25} />
                <p className="text-[12px] font-medium">Account</p>
              </Link>
            ) : (
              <Link
                to="/account"
                className={`flex flex-col items-center ${
                  location.pathname === '/account' ? 'text-[#ec6525]' : ''
                }`}
              >
                <BsFillPersonFill size={25} />
                <p className="text-[12px] font-medium">Account</p>
              </Link>
            )}

            {isAuthenticated && (
              <>
                <Link
                  to="/wishlist"
                  className={`flex flex-col items-center ${
                    location.pathname === '/wishlist' ? 'text-[#ec6525]' : ''
                  }`}
                >
                  <BsHeartFill size={25} />
                  <p className="text-[12px] font-medium">Wishlist</p>
                </Link>

                <Link
                  to="/order-history"
                  className={`flex flex-col items-center ${
                    location.pathname === '/order-history'
                      ? 'text-[#ec6525]'
                      : ''
                  }`}
                >
                  <IoIosCube size={25} />
                  <p className="text-[12px] font-medium">Orders</p>
                </Link>
              </>
            )}
          </div>
        </div>
      ) : (
        <ul className="bg-white h-16 z-20 text-gray-700 fixed bottom-0 left-0 right-0 max-w-screen md:hidden flex gap-2 justify-around items-center">
          <li onClick={() => onMoveWhishListItem(data)}>
            {!isAuthenticated ? (
              <button
                className="border border-gray-300 px-4 py-3 rounded-lg"
                onClick={handleClick}
              >
                <BsHeartFill className="text-gray-600" size={20} />
              </button>
            ) : (
              <button className="border border-gray-300 px-4 py-3 rounded-lg">
                {data?.isFavorite ? (
                  <BsHeartFill className="text-pink-500" size={20} />
                ) : (
                  <BsHeartFill className="text-gray-600" size={20} />
                )}
              </button>
            )}
          </li>
          <li className="w-40">
            {addCart === false && !foundCartProduct ? (
              <button
                onClick={() => {
                  if (itemChecked) {
                    addProductToCart(data);
                  } else {
                    dispatch(success('Select Variant First'));
                  }
                }}
                className="bg-[#FF9F00] w-full text-white font-medium px-2 py-3 rounded-lg flex items-center justify-center gap-1"
              >
                <BsFillBasket2Fill />
                Add To Cart
              </button>
            ) : (
              <Link
                to="/cart"
                className="bg-[#FF9F00] w-full text-white font-medium px-2 py-3 rounded-lg flex items-center justify-center gap-1"
              >
                <BsFillBasket2Fill />
                GO TO CART
              </Link>
            )}
          </li>
          <li className="w-40">
            {isAuthenticated && data?.stock > 0 ? (
              <button
                className="bg-[#FB641B] flex items-center justify-center w-full text-white font-medium px-2 py-3 rounded-lg gap-1"
                onClick={() => {
                  if (itemChecked) {
                    buyProduct(data);
                  } else {
                    dispatch(success('Select Variant First'));
                  }
                }}
              >
                <BsCart2 />
                BUY NOW
              </button>
            ) : (
              <button
                className="bg-[#FB641B] flex items-center justify-center w-full text-white font-medium px-2 py-3 rounded-lg gap-1"
                onClick={handleCheckout}
              >
                <BsCart2 />
                BUY NOW
              </button>
            )}
          </li>
        </ul>
      )}
    </>
  );
}

关键修复点说明

  • 移除scrollDirection状态:直接在scroll事件里计算滚动方向,避免闭包和状态依赖问题。
  • 使用useRef保存滚动状态:prevScrollY和throttleTimeout用ref存储,避免闭包捕获旧值,同时不触发组件重渲染。
  • 节流处理scroll事件:限制事件触发频率,减少状态更新次数,提升移动端性能。
  • 放宽顶部判断阈值:用currentScrollY <=10替代===0,适配移动端滚动惯性导致的scrollY无法精确到0的情况。
  • 添加passive事件选项:告诉浏览器滚动事件不会调用preventDefault(),让浏览器优化滚动性能,这在移动端非常重要。

移动端React滚动事件处理经验

  • 优先节流而非防抖:节流保证滚动过程中持续响应,防抖适合滚动结束后触发的操作(比如加载更多)。
  • 避免在scroll事件中触发重渲染:尽量用ref存储临时状态,减少组件不必要的重渲染。
  • 适配浏览器滚动行为:部分浏览器支持弹性滚动,scrollY可能出现负数,判断顶部时要考虑这种情况(比如currentScrollY <= 0)。
  • 测试真实设备:模拟器和真实设备的滚动行为有差异,一定要在真机上测试不同系统、浏览器的表现。
  • 避免依赖window.scrollY的精确值:移动端滚动时scrollY是连续变化的,用范围判断比精确值更可靠。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 08:14:55