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

React Next.js中Swiper缩略图导航构建模式失效问题求助

解决Next.js构建后Swiper缩略图导航失效问题

问题根源

  1. SSR/SSG初始化时机差异:开发模式为纯客户端渲染,Swiper能正常绑定缩略图实例;生产构建时Next.js会进行服务端渲染,此时thumbsSwiper初始为null,后续客户端hydration时组件重复挂载导致实例绑定失效。
  2. 重复渲染主Swiper:原代码根据thumbsSwiper状态渲染两个不同的Swiper组件,破坏了React的hydration一致性,导致生产环境下实例关联失败。
  3. 缺失关键配置:缩略图Swiper未启用slideToClickedSlide,点击缩略图无法触发主Swiper切换;同时未明确指定初始幻灯片,导致激活状态异常。

解决方案

1. 合并主Swiper渲染逻辑

删除条件渲染的两个Swiper组件,统一为一个,通过thumbs属性的条件赋值实现实例绑定,避免组件重复挂载:

<Swiper
  id="productGallery"
  thumbs={thumbsSwiper ? { swiper: thumbsSwiper } : undefined}
  modules={[Navigation, Thumbs]}
  navigation={{
    prevEl: prevRef.current!,
    nextEl: nextRef.current!,
  }}
  initialSlide={0} // 明确初始幻灯片
  {...swiperParams}
>
  {/* 幻灯片内容保持不变 */}
</Swiper>

2. 禁用Swiper组件的SSR渲染

Swiper依赖DOM环境,SSR下无法正确初始化。使用next/dynamic动态导入Swiper组件并禁用SSR:

import dynamic from 'next/dynamic'

// 动态导入Swiper相关组件,禁用SSR
const Swiper = dynamic(() => import('swiper/react').then(mod => mod.Swiper), { ssr: false })
const SwiperSlide = dynamic(() => import('swiper/react').then(mod => mod.SwiperSlide), { ssr: false })
const { Navigation, Thumbs } = require('swiper')

3. 完善缩略图Swiper配置

添加slideToClickedSlide确保点击缩略图能切换主Swiper,同时明确初始幻灯片:

<Swiper
  id="productGalleryThumbs"
  modules={[Navigation, Thumbs]}
  onSwiper={setThumbsSwiper}
  spaceBetween={0}
  watchSlidesProgress
  freeMode
  observer
  observeParents
  initialSlide={0} // 同步初始幻灯片
  slideToClickedSlide={true} // 启用点击缩略图切换
  breakpoints={galleryCarouselBreakpoints}
>
  {/* 缩略图幻灯片内容保持不变 */}
</Swiper>

4. 确保Swiper样式完整导入

在_app.tsx或全局样式文件中导入完整的Swiper样式,避免生产构建时样式被tree shake:

import 'swiper/css'
import 'swiper/css/thumbs'
import 'swiper/css/navigation'

修改后的完整组件代码

import dynamic from 'next/dynamic'
import { productGalleryPlaceholder } from '@/public/assets/placeholders'
import { getDirection } from '@/utils/getDirection'
import cn from 'classnames'
import { Attachment } from 'lib/types/products'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useRef, useState } from 'react'
import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io'
import Scrollbar from '../Scrollbar'

// 动态导入Swiper组件,禁用SSR
const Swiper = dynamic(() => import('swiper/react').then(mod => mod.Swiper), { ssr: false })
const SwiperSlide = dynamic(() => import('swiper/react').then(mod => mod.SwiperSlide), { ssr: false })
const { Navigation, Thumbs } = require('swiper')

interface Props {
  gallery: Attachment[]
  thumbnailClassName?: string
  galleryClassName?: string
}

const galleryCarouselBreakpoints = {
  '0': {
    slidesPerView: 4,
  },
}

const swiperParams = {
  slidesPerView: 1,
  spaceBetween: 0,
}

const ThumbnailCarousel: React.FC<Props> = ({
  gallery,
  thumbnailClassName = 'xl:w-[480px] 2xl:w-[650px]',
  galleryClassName,
}) => {
  const [thumbsSwiper, setThumbsSwiper] = useState<any>(null)
  const prevRef = useRef<HTMLDivElement>(null)
  const nextRef = useRef<HTMLDivElement>(null)
  const { locale } = useRouter()
  const dir = getDirection(locale)

  return (
    <div className="w-full xl:flex xl:flex-row-reverse">
      <div
        className={cn(
          'relative mb-2.5 w-full overflow-hidden rounded-md border border-border-base md:mb-3 xl:ltr:ml-5 xl:rtl:mr-5',
          thumbnailClassName
        )}
      >
        <Swiper
          id="productGallery"
          thumbs={thumbsSwiper ? { swiper: thumbsSwiper } : undefined}
          modules={[Navigation, Thumbs]}
          navigation={{
            prevEl: prevRef.current!,
            nextEl: nextRef.current!,
          }}
          initialSlide={0}
          {...swiperParams}
        >
          {gallery?.map((item) => (
            <SwiperSlide
              key={`product-gallery-${item.id}`}
              className="flex items-center justify-center"
            >
              <Image
                src={item?.original ?? productGalleryPlaceholder}
                alt={`Product gallery ${item.id}`}
                width={540}
                height={440}
                className="rounded-lg object-contain"
              />
            </SwiperSlide>
          ))}
        </Swiper>
        <div className="absolute top-2/4 z-10 flex w-full items-center justify-between px-2.5">
          <div
            ref={prevRef}
            className="flex h-7 w-7 -translate-y-1/2 transform cursor-pointer items-center justify-center rounded-full bg-brand-light text-base shadow-navigation transition duration-300 hover:bg-brand hover:text-brand-light focus:outline-none md:h-8 md:w-8 lg:h-9 lg:w-9 lg:text-lg xl:h-10 xl:w-10 xl:text-xl"
          >
            {dir === 'rtl' ? <IoIosArrowForward /> : <IoIosArrowBack />}
          </div>
          <div
            ref={nextRef}
            className="flex h-7 w-7 -translate-y-1/2 transform cursor-pointer items-center justify-center rounded-full bg-brand-light text-base shadow-navigation transition duration-300 hover:bg-brand hover:text-brand-light focus:outline-none md:h-8 md:w-8 lg:h-9 lg:w-9 lg:text-lg xl:h-10 xl:w-10 xl:text-xl"
          >
            {dir === 'rtl' ? <IoIosArrowBack /> : <IoIosArrowForward />}
          </div>
        </div>
      </div>

      <Scrollbar
        options={{
          overflow: { y: 'scroll' },
          scrollbars: {
            visibility: 'visible',
          },
        }}
      >
        <div className={`shrink-0 ${galleryClassName}`}>
          <Swiper
            id="productGalleryThumbs"
            modules={[Navigation, Thumbs]}
            onSwiper={setThumbsSwiper}
            spaceBetween={0}
            watchSlidesProgress
            freeMode
            observer
            observeParents
            initialSlide={0}
            slideToClickedSlide={true}
            breakpoints={galleryCarouselBreakpoints}
          >
            {gallery?.map((item) => (
              <SwiperSlide
                key={`product-thumb-gallery-${item.id}`}
                className="flex cursor-pointer items-center justify-center overflow-hidden rounded border border-border-base transition hover:opacity-75"
              >
                <Image
                  src={item?.thumbnail ?? productGalleryPlaceholder}
                  alt={`Product thumb gallery ${item.id}`}
                  width={70}
                  height={70}
                />
              </SwiperSlide>
            ))}
          </Swiper>
        </div>
      </Scrollbar>
    </div>
  )
}

export default ThumbnailCarousel

验证方法

执行yarn build && yarn start,检查以下两点:

  • 初始加载时第一个幻灯片处于激活状态
  • 点击缩略图和箭头均可正常切换幻灯片

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 06:43:10