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

React带缩略图的Swiper切换路由时幻灯片计数错误(新增而非替换)

React Router动态路由切换后Swiper主幻灯片追加而非替换的问题修复

问题场景

基于react-router-dom开发的商品详情页为动态路由,切换不同颜色商品时,路由更新并触发商品数据请求,数据更新后,缩略图Swiper显示正常,但主Swiper未替换原有幻灯片,而是将新幻灯片追加到原有内容后。尝试过useEffect销毁重初始化Swiper、调用swiper.update()、监听onDestroy/onInit事件等方法,均未解决。

核心原因分析

  1. Swiper实例引用未重置:路由切换后,旧的thumbsSwiper实例仍被主Swiper引用,导致主Swiper无法正确识别新的幻灯片集合。
  2. React节点复用逻辑冲突:主Swiper的SwiperSlide使用image.product_id作为key,不同商品的图片可能共享相同的product_id,导致React复用旧的Slide节点,未渲染新内容。
  3. 组件未触发重新挂载:路由切换时,ConsumableCardPreview组件未触发完全重新挂载,Swiper内部状态未清空。

解决方案

  1. 给主Swiper和缩略图Swiper添加唯一key,绑定商品的唯一标识(如consumable.id),确保路由切换时组件完全重新挂载。
  2. 修改主SwiperSlide的key为图片的唯一值(如image.image),避免key重复导致的节点复用。
  3. 监听consumable变化,重置swiper实例引用和滑动状态,解除旧实例的关联。

修改后的组件代码

import { FC, useState, useEffect } from 'react'
import { Swiper, SwiperSlide } from 'swiper/react'

// Import Swiper styles
import 'swiper/css'
import 'swiper/css/free-mode'
import 'swiper/css/navigation'
import 'swiper/css/thumbs'

import SwiperCore, { FreeMode, Navigation, Pagination, Thumbs } from 'swiper'
import { IConsumableData } from '../../../../../../types/api'
import { BASE_MEDIA } from '../../../../../../api/_variables'
import NavArrow, { NavArrowStatus } from '../../../../../UI/IconsSvg/NavArrow/NavArrow'
import ExpandIcon from '../../../../../UI/IconsSvg/ExpandIcon/ExpandIcon'

import 'react-medium-image-zoom/dist/styles.css'
import styles from './ConsumableCardPreview.module.scss'
import './ConsumableCardPreviewSwiper.scss'

interface ConsumableCardPreviewProps {
  consumable: IConsumableData
}

const ConsumableCardPreview: FC<ConsumableCardPreviewProps> = ({ consumable }) => {
  const [thumbsSwiper, setThumbsSwiper] = useState<SwiperCore>()
  const [mainSwiper, setMainSwiper] = useState<SwiperCore>()
  const [currentSlide, setCurrentSlide] = useState(0)
  const [isZoomed, setIsZoomed] = useState(false)

  // 监听商品变化,重置swiper实例和滑动状态
  useEffect(() => {
    setThumbsSwiper(null)
    setMainSwiper(null)
    setCurrentSlide(0)
  }, [consumable.id])

  const handlePrev = () => {
    mainSwiper?.slidePrev()
  }

  const handleNext = () => {
    mainSwiper?.slideNext()
  }

  return (
    <div className={styles.previewContainer}>
      <div className={styles.consumableSwipersWrapper}>
        {/* 绑定商品唯一ID作为Swiper的key,强制重新挂载 */}
        <Swiper
          key={consumable.id}
          onSwiper={setMainSwiper}
          spaceBetween={10}
          navigation
          pagination={{
            type: 'bullets',
            clickable: false,
            renderBullet: () => '<span class="swiper-pagination-bullet"></span>'
          }}
          thumbs={{ swiper: thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null }}
          modules={[FreeMode, Thumbs, Navigation, Pagination]}
          className={styles.mainConsumableSwiper}
          onSlideChange={(swiper) => {
            setCurrentSlide(swiper.realIndex)
          }}
        >
          {/* 使用图片路径作为唯一key,避免节点复用 */}
          {consumable.images.map((image) => (
            <SwiperSlide className={styles.mainSwiperSlide} key={image.image}>
              <img src={`${BASE_MEDIA}${image.image}`} className={styles.mainSwiperImage} />
              <ExpandIcon className={styles.zoomIcon} onClick={() => setIsZoomed(true)} />
            </SwiperSlide>
          ))}
        </Swiper>

        <div className={styles.thumbsWrapper}>
          <div className={styles.thumbsPrev}>
            <NavArrow
              status={currentSlide === 0 ? NavArrowStatus.DISABLED : NavArrowStatus.ACTIVE}
              onClick={handlePrev}
            />
          </div>
          {/* 缩略图Swiper同样绑定商品唯一ID */}
          <Swiper
            key={consumable.id}
            modules={[FreeMode, Navigation, Thumbs]}
            onSwiper={setThumbsSwiper}
            spaceBetween={10}
            slidesPerView={3}
            direction={'vertical'}
            navigation={false}
            watchSlidesProgress={true}
            className={styles.thumbsConsumableSwiper}
            breakpoints={{
              800: {
                slidesPerView: 4
              },
              1000: {
                slidesPerView: 3
              }
            }}
          >
            {consumable.images.map((image) => (
              <SwiperSlide className={styles.thumbsSlide} key={image.image}>
                <img src={`${BASE_MEDIA}${image.image}`} />
              </SwiperSlide>
            ))}
          </Swiper>
          <div className={styles.thumbsNext}>
            <NavArrow
              status={
                currentSlide === consumable.images.length - 1
                  ? NavArrowStatus.DISABLED
                  : NavArrowStatus.ACTIVE
              }
              onClick={handleNext}
            />
          </div>
        </div>
      </div>
    </div>
  )
}

export default ConsumableCardPreview

样式代码(无修改)

@use '../../ConsumableCardContent.module.scss' as column;
@use '/src/styles/variables' as var;

$gap: 3.75rem;

.previewContainer {
  position: relative;
}

.consumableSwipersWrapper {
  width: 100%;
  display: grid;
  grid-template-columns: calc(column.$autoColumnWidth - $gap) 1fr;
  gap: $gap;
  position: absolute;
  inset: 0;

  @media (max-width: var.$tablet-size) {
    padding-inline: 1.875rem;
    position: relative;
    gap: 2rem;
    height: 100%;
    grid-template-columns: minmax(auto, 6.25rem) 1fr;
  }

  @media (max-width: var.$mobile-size) {
    padding-inline: 0;
    grid-template-columns: 1fr;
    min-height: 0;
  }
}

.mainConsumableSwiper {
  width: 100%;
  grid-column: 2 / 3;

  @media (max-width: var.$mobile-size) {
    grid-column: 1 / -1;
  }
}

.mainSwiperSlide {
  cursor: grab;
  position: relative;
}

.mainSwiperImage {
  width: 100%;
  height: 100%;
  max-width: 100%;
  object-fit: cover;
  pointer-events: none;

  @media (max-width: var.$mobile-size) {
    object-fit: contain;
  }
}

.zoomIcon {
  position: absolute;
  top: 2%;
  left: 2%;
  z-index: 1;
}

.thumbsWrapper {
  order: -1;
  grid-column: 1 / 2;
  display: flex;
  flex-direction: column;
  position: absolute;
  inset: 0;

  @media (max-width: var.$mobile-size) {
    display: none;
  }
}

.thumbsConsumableSwiper {
  width: 100%;
  height: 75%;
  max-height: 100%;
  margin-block: auto;
}

.thumbsSlide {
  opacity: 0.5;
  height: 25%;
  cursor: pointer;
  @media (max-width: var.$tablet-size) {
    max-width: 6.25rem;
    aspect-ratio: 1 / 1;
  }
}

.thumbsPrev,
.thumbsNext {
  display: grid;
  place-items: center;
  cursor: pointer;

  & > img {
    pointer-events: none;
  }
}

.thumbsNext {
  transform: rotate(180deg);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 02:45:22