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

React弹性效果轮播组件状态异常问题咨询与优化方案

带有弹性效果的React轮播组件问题与解决方案

问题描述

开发的React轮播组件采用以下逻辑实现弹性切换:点击按钮时先让滑块移动550px,延迟600ms后回调移动回50px,模拟弹性回弹效果。正常使用时功能正常,但快速点击NEXT按钮触发从最后一张幻灯片重置到第一张(dist归0)时,会出现状态混乱、setState语句丢失的问题。需要通过Promise或其他方式修复该问题,同时咨询是否可仅用CSS或React库实现类似弹性效果。

现有实现代码

JSX代码

import "./SlideShow.css";
import { useState } from "react";
import { Button } from "@mui/material";

function SlideShow() {
  const [dist, setDist] = useState(0);

  console.log(dist);

  const delay = (t) => new Promise((resolve) => setTimeout(resolve, t));
  const n = 6;
  return (
    <div className="container">
      <div className="left"></div>
      <div className="slideshow">
        <div className="view">
          <div
            className="images"
            style={{ transform: `translate(-${dist}px,0)` }}
          >
            <div className="img">a</div>
            <div className="img">b</div>
            <div className="img">c</div>
            <div className="img">d</div>
            <div className="img">e</div>
            <div className="img">f</div>
            <div className="img">g</div>
          </div>
        </div>
        <div className="buttons">
          <Button
            onClick={() => {
              if (dist / 500 == 0) setDist(n * 500);
              else {
                setDist((d) => d - 550);
                delay(600).then(() => setDist((d) => d + 50));
              }
            }}
          >
            PREV
          </Button>
          <Button
            onClick={() => {
              if (dist / 500 >= n) setDist(0);
              else {
                setDist((d) => d + 550);
                delay(600).then(() => setDist((d) => d - 50));
              }
            }}
          >
            NEXT
          </Button>
        </div>
      </div>
      <div className="right"></div>
    </div>
  );
}

export default SlideShow;

CSS代码

.container {
  width: 100vw;
  height: 100vh;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
}
.left {
  flex-grow: 1;
  background-color: white;
  height: 100%;
  z-index: 2;
}

.right {
  flex-grow: 1;
  background-color: white;
  height: 100%;
  z-index: 2;
}
.view {
  width: 500px;
  height: 500px;
  background-color: beige;
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
  border-radius: 2px;
  overflow-x: hidden;
}

.slideshow {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

body {
  overflow-x: hidden;
  position: relative;
}

.img {
  min-width: 500px;
  height: 500px;
  display: grid;
  place-content: center;
  border: 0px solid red;
}

.images {
  position: absolute;
  display: flex;
  flex-direction: row;
  width: 100vw;
  height: 500px;
  transition: 750ms;
}

问题分析与修复方案

问题根源:快速点击时,之前触发的延迟Promise仍处于pending状态,当执行重置dist到0的操作后,旧的Promise回调会修改状态,导致状态混乱。

修复实现(使用状态锁+Timeout管理)

通过添加动画状态锁、管理Timeout实例,避免并发动画干扰:

import "./SlideShow.css";
import { useState, useRef } from "react";
import { Button } from "@mui/material";

function SlideShow() {
  const [dist, setDist] = useState(0);
  const [isAnimating, setIsAnimating] = useState(false);
  const timeoutRef = useRef(null); // 保存Timeout实例,用于取消旧回调

  const n = 6;
  const slideWidth = 500;
  const bounceOffset = 50;

  const handlePrev = () => {
    if (isAnimating) return; // 动画进行中时禁止触发新操作
    setIsAnimating(true);

    // 清除之前未执行的延迟回调
    if (timeoutRef.current) clearTimeout(timeoutRef.current);

    if (dist / slideWidth === 0) {
      setDist(n * slideWidth);
      setIsAnimating(false);
    } else {
      setDist(d => d - (slideWidth + bounceOffset));
      timeoutRef.current = setTimeout(() => {
        setDist(d => d + bounceOffset);
        setIsAnimating(false);
        timeoutRef.current = null;
      }, 600);
    }
  };

  const handleNext = () => {
    if (isAnimating) return;
    setIsAnimating(true);

    if (timeoutRef.current) clearTimeout(timeoutRef.current);

    if (dist / slideWidth >= n) {
      setDist(0);
      setIsAnimating(false);
    } else {
      setDist(d => d + (slideWidth + bounceOffset));
      timeoutRef.current = setTimeout(() => {
        setDist(d => d - bounceOffset);
        setIsAnimating(false);
        timeoutRef.current = null;
      }, 600);
    }
  };

  return (
    <div className="container">
      <div className="left"></div>
      <div className="slideshow">
        <div className="view">
          <div
            className="images"
            style={{ transform: `translate(-${dist}px,0)` }}
          >
            <div className="img">a</div>
            <div className="img">b</div>
            <div className="img">c</div>
            <div className="img">d</div>
            <div className="img">e</div>
            <div className="img">f</div>
            <div className="img">g</div>
          </div>
        </div>
        <div className="buttons">
          <Button onClick={handlePrev}>PREV</Button>
          <Button onClick={handleNext}>NEXT</Button>
        </div>
      </div>
      <div className="right"></div>
    </div>
  );
}

export default SlideShow;

替代实现方式

仅用CSS实现弹性效果

无需JS的延迟回调,直接通过CSS的cubic-bezier过渡曲线实现弹性回弹:
修改.images的过渡属性:

.images {
  /* 其他样式保持不变 */
  transition: transform 0.75s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}

对应的JS逻辑可以简化为直接切换目标位置:

const handlePrev = () => {
  if (dist / slideWidth === 0) {
    setDist(n * slideWidth);
  } else {
    setDist(d => d - slideWidth);
  }
};

const handleNext = () => {
  if (dist / slideWidth >= n) {
    setDist(0);
  } else {
    setDist(d => d + slideWidth);
  }
};

使用React轮播库实现

借助成熟库快速实现弹性效果,无需手动处理动画逻辑:
以swiper/react为例:

import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';
import 'swiper/css/effect-bounce';
import { EffectBounce, Navigation } from 'swiper/modules';

function SlideShow() {
  return (
    <Swiper
      effect={'bounce'}
      slidesPerView={1}
      loop={true}
      navigation={true}
      modules={[EffectBounce, Navigation]}
      style={{ width: 500, height: 500 }}
    >
      <SwiperSlide style={{ backgroundColor: 'beige', display: 'grid', placeContent: 'center' }}>a</SwiperSlide>
      <SwiperSlide style={{ backgroundColor: 'beige', display: 'grid', placeContent: 'center' }}>b</SwiperSlide>
      <SwiperSlide style={{ backgroundColor: 'beige', display: 'grid', placeContent: 'center' }}>c</SwiperSlide>
      <SwiperSlide style={{ backgroundColor: 'beige', display: 'grid', placeContent: 'center' }}>d</SwiperSlide>
      <SwiperSlide style={{ backgroundColor: 'beige', display: 'grid', placeContent: 'center' }}>e</SwiperSlide>
      <SwiperSlide style={{ backgroundColor: 'beige', display: 'grid', placeContent: 'center' }}>f</SwiperSlide>
      <SwiperSlide style={{ backgroundColor: 'beige', display: 'grid', placeContent: 'center' }}>g</SwiperSlide>
    </Swiper>
  );
}

export default SlideShow;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 07:15:35