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

React中轮播图点击切换时出现闪烁问题求助

React+Tailwind轮播切换闪烁/残影问题修复

问题描述

React+Tailwind项目中实现的轮播功能,点击下一页按钮时图片会出现闪烁;仅两张图片切换时,会出现前/后一张图的残影。每次操作都会触发闪烁,但录屏仅能捕捉到第一次的闪烁现象。

现有代码

Index.css

.slide-in-from-right {
  animation: slideInFromRight 0.5s ease-in;
}

.slide-out-to-left {
  animation: slideOutToLeft 0.5s ease-in;
}

@keyframes slideInFromRight {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0%);
  }
}

@keyframes slideOutToLeft {
  from {
    transform: translateX(0%);
  }
  to {
    transform: translateX(-100%);
  }
}

HeroSlideshow组件

let count = 0;
export default function HeroSlideshow() {
  const [slide, setSlide] = useState({});
  const [clonedSlide, setClonedSlide] = useState({});
  const [slides, setSlides] = useState([]);
  const [currentIndex, setCurrentIndex] = useState(0);
  const slideRef = useRef();
  const clonedSlideRef = useRef();

  const { updateNotification } = useNotification();

  const fetchLatestUploads = async () => {
    const { error, movies } = await getLatestUploads();
    if (error) return updateNotification("error", error);

    setSlides([...movies]);
    setSlide(movies[0]);
  };

  const handleOnNextClick = () => {
    setClonedSlide(slides[count]);
    clonedSlideRef.current.classList.add("slide-out-to-left");
    count = (count + 1) % slides.length;
    setSlide(slides[count]);
    setCurrentIndex(count);

    slideRef.current.classList.add("slide-in-from-right");
  };

  const handleAnimationEnd = () => {
    slideRef.current.classList.remove("slide-in-from-right");
    clonedSlideRef.current.classList.remove("slide-out-to-left");
    setClonedSlide({});
  };

  useEffect(() => {
    fetchLatestUploads();
  }, []);

  return (
    <div className="w-full flex">
      <div className="w-4/5 aspect-video relative overflow-hidden">
        <img
          ref={slideRef}
          onAnimationEnd={handleAnimationEnd}
          className="aspect-video object-cover"
          src={slide.poster}
          alt=""
        />
        <img
          ref={clonedSlideRef}
          onAnimationEnd={handleAnimationEnd}
          className="aspect-video object-cover absolute inset-0"
          src={clonedSlide.poster}
          alt=""
        />

        <SlideShowController onNextClick={handleOnNextClick} />
      </div>

      <div className="w-1/5 aspect-video bg-red-300"></div>
    </div>
  );
}

const SlideShowController = ({ onNextClick, onPrevClick }) => {
  const btnClass =
    "bg-primary rounded border-2 text-white text-xl p-2 outline-none";
  return (
    <div className="absolute top-1/2 -translate-y-1/2 w-full flex items-center justify-between px-2">
      <button onClick={onPrevClick} className={btnClass} type="button">
        <AiOutlineDoubleLeft />
      </button>
      <button onClick={onNextClick} className={btnClass} type="button">
        <AiOutlineDoubleRight />
      </button>
    </div>
  );
};

问题根源

  1. 全局变量count未被React追踪:状态更新不同步,导致切换逻辑出现时序错误
  2. 动画结束时直接清空clonedSlide:导致DOM中出现src为空的图片,引发闪烁
  3. 两张图切换时状态逻辑冲突:克隆图与当前图的切换时机不匹配,产生残影
  4. 动画未等待图片加载完成:图片未加载就执行动画,出现空白闪烁

修复方案

1. 替换全局变量为React状态

将let count = 0改为const [count, setCount] = useState(0),让React追踪状态变化,确保逻辑同步。

2. 优化切换逻辑时序

先保存当前slide作为克隆图,再更新count和当前slide,避免状态混乱。

3. 图片加载完成后触发动画

添加onLoad事件监听,确保图片加载完成后再执行进入动画,避免空白闪烁。

4. 区分动画结束事件

分别处理两个图片元素的动画结束事件,避免互相干扰;动画结束后仅移除类名,不清空克隆图状态,直到下一次切换更新。

5. 条件渲染克隆图

仅当克隆图存在有效poster时才渲染,避免空src图片出现。

6. 优化动画过渡效果

为动画添加透明度变化,让切换更平滑,减少残影感。

修改后的完整代码

优化后的Index.css

.slide-in-from-right {
  animation: slideInFromRight 0.5s ease-in-out forwards;
}

.slide-out-to-left {
  animation: slideOutToLeft 0.5s ease-in-out forwards;
}

@keyframes slideInFromRight {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0%);
    opacity: 1;
  }
}

@keyframes slideOutToLeft {
  from {
    transform: translateX(0%);
    opacity: 1;
  }
  to {
    transform: translateX(-100%);
    opacity: 0;
  }
}

修改后的HeroSlideshow组件

import { useState, useEffect, useRef } from 'react';
import { AiOutlineDoubleLeft, AiOutlineDoubleRight } from 'react-icons/ai';
import { useNotification } from './你的路径'; // 替换为实际路径
import { getLatestUploads } from './你的路径'; // 替换为实际路径

export default function HeroSlideshow() {
  const [slide, setSlide] = useState({});
  const [clonedSlide, setClonedSlide] = useState({});
  const [slides, setSlides] = useState([]);
  const [currentIndex, setCurrentIndex] = useState(0);
  const [count, setCount] = useState(0);
  const slideRef = useRef();
  const clonedSlideRef = useRef();
  const [isSlideLoaded, setIsSlideLoaded] = useState(false);

  const { updateNotification } = useNotification();

  const fetchLatestUploads = async () => {
    const { error, movies } = await getLatestUploads();
    if (error) return updateNotification("error", error);

    setSlides([...movies]);
    setSlide(movies[0]);
  };

  const handleOnNextClick = () => {
    // 保存当前slide作为克隆图
    setClonedSlide(slides[count]);
    // 计算新的索引
    const newCount = (count + 1) % slides.length;
    setCount(newCount);
    setCurrentIndex(newCount);
    // 设置新的slide
    setSlide(slides[newCount]);
    // 触发克隆图的退出动画
    if (clonedSlideRef.current) {
      clonedSlideRef.current.classList.add("slide-out-to-left");
    }
    // 重置图片加载状态
    setIsSlideLoaded(false);
  };

  const handleSlideLoad = () => {
    setIsSlideLoaded(true);
    // 图片加载完成后触发进入动画
    if (slideRef.current) {
      slideRef.current.classList.add("slide-in-from-right");
    }
  };

  const handleAnimationEnd = (e) => {
    // 区分两个元素的动画结束事件
    if (e.target === slideRef.current) {
      slideRef.current.classList.remove("slide-in-from-right");
    } else if (e.target === clonedSlideRef.current) {
      clonedSlideRef.current.classList.remove("slide-out-to-left");
    }
  };

  useEffect(() => {
    fetchLatestUploads();
  }, []);

  return (
    <div className="w-full flex">
      {/* Slide show section */}
      <div className="w-4/5 aspect-video relative overflow-hidden">
        <img
          ref={slideRef}
          onLoad={handleSlideLoad}
          onAnimationEnd={handleAnimationEnd}
          className="aspect-video object-cover w-full"
          src={slide.poster}
          alt={slide.title || "轮播图片"}
        />
        {/* 条件渲染克隆图 */}
        {clonedSlide.poster && (
          <img
            ref={clonedSlideRef}
            onAnimationEnd={handleAnimationEnd}
            className="aspect-video object-cover absolute inset-0 w-full"
            src={clonedSlide.poster}
            alt={clonedSlide.title || "克隆轮播图"}
          />
        )}

        <SlideShowController onNextClick={handleOnNextClick} />
      </div>

      {/* Up Next Section */}
      <div className="w-1/5 aspect-video bg-red-300"></div>
    </div>
  );
}

const SlideShowController = ({ onNextClick, onPrevClick }) => {
  const btnClass =
    "bg-primary rounded border-2 text-white text-xl p-2 outline-none hover:bg-opacity-90 transition-colors";
  return (
    <div className="absolute top-1/2 -translate-y-1/2 w-full flex items-center justify-between px-2">
      <button onClick={onPrevClick} className={btnClass} type="button">
        <AiOutlineDoubleLeft />
      </button>
      <button onClick={onNextClick} className={btnClass} type="button">
        <AiOutlineDoubleRight />
      </button>
    </div>
  );
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 08:05:25