React带缩略图的Swiper切换路由时幻灯片计数错误(新增而非替换)
React Router动态路由切换后Swiper主幻灯片追加而非替换的问题修复
问题场景
基于react-router-dom开发的商品详情页为动态路由,切换不同颜色商品时,路由更新并触发商品数据请求,数据更新后,缩略图Swiper显示正常,但主Swiper未替换原有幻灯片,而是将新幻灯片追加到原有内容后。尝试过useEffect销毁重初始化Swiper、调用swiper.update()、监听onDestroy/onInit事件等方法,均未解决。
核心原因分析
- Swiper实例引用未重置:路由切换后,旧的
thumbsSwiper实例仍被主Swiper引用,导致主Swiper无法正确识别新的幻灯片集合。 - React节点复用逻辑冲突:主Swiper的
SwiperSlide使用image.product_id作为key,不同商品的图片可能共享相同的product_id,导致React复用旧的Slide节点,未渲染新内容。 - 组件未触发重新挂载:路由切换时,
ConsumableCardPreview组件未触发完全重新挂载,Swiper内部状态未清空。
解决方案
- 给主Swiper和缩略图Swiper添加唯一key,绑定商品的唯一标识(如
consumable.id),确保路由切换时组件完全重新挂载。 - 修改主SwiperSlide的key为图片的唯一值(如
image.image),避免key重复导致的节点复用。 - 监听
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
相关产品推荐
相关产品推荐

