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

Next.js Hydration错误求助:React Swiper引发服务端客户端内容不匹配

React Swiper 导致 Hydration 错误的原因及解决方法

问题现象

使用 React Swiper 实现轮播功能时触发 Hydration 错误,控制台提示:

Warning: Text content did not match. Server: "Zach N." Client: "Lisa A."

注释 Swiper 相关代码后错误消失。

错误原因

  1. loop: true 的机制冲突:开启循环轮播时,Swiper 会在服务端渲染阶段自动复制首尾 Slide(将最后一个 Slide 前置、第一个 Slide 后置),导致服务端输出的初始可见 Slide 是最后一个(Zach N.),但客户端初始化后默认显示第一个 Slide(Lisa A.),两者文本内容不匹配,触发 React 的 Hydration 校验失败。
  2. 环境差异导致状态不一致:Swiper 依赖浏览器的 DOM 和窗口环境计算轮播状态,服务端渲染时无法完成这些计算,最终导致服务端输出的 HTML 与客户端 Hydration 后的 DOM 内容/结构不匹配。

解决方法

方法1:仅在客户端渲染 Swiper 组件

利用 React 的状态控制或动态导入,让 Swiper 只在浏览器环境中渲染,规避服务端与客户端的状态差异:

修改后的核心代码示例:

import { useState, useEffect, useRef, useCallback } from "react";
import AOS from "aos";
import "aos/dist/aos.css";

// 动态导入Swiper组件,仅在客户端加载
const Swiper = React.lazy(() => import("swiper/react").then((mod) => ({ default: mod.Swiper })));
const SwiperSlide = React.lazy(() => import("swiper/react").then((mod) => ({ default: mod.SwiperSlide })));
import "swiper/css";

function Testimonials() {
  const sliderRef = useRef(null);
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    // 标记当前为客户端环境
    setIsClient(true);
    AOS.init({
      duration: 1500,
    });
  }, []); // 修复AOS初始化逻辑,添加空依赖数组避免重复执行

  const handlePrev = useCallback(() => {
    if (!sliderRef.current) return;
    sliderRef.current.swiper.slidePrev();
  }, []);

  const handleNext = useCallback(() => {
    if (!sliderRef.current) return;
    sliderRef.current.swiper.slideNext();
  }, []);

  return (
    <div>
      {/* 其余页面代码保持不变 */}
      
      {/* 仅在客户端渲染Swiper */}
      <div className="md:flex">
        {isClient && (
          <React.Suspense fallback={<div>加载中...</div>}>
            <Swiper
              ref={sliderRef}
              loop={true}
              pagination={{ clickable: true }}
              slidesPerView={1}
              spaceBetween={10}
              breakpoints={{
                "@0.00": { slidesPerView: 1, spaceBetween: 10 },
                "@0.75": { slidesPerView: 2, spaceBetween: 20 },
                "@1.00": { slidesPerView: 3, spaceBetween: 40 },
                "@1.50": { slidesPerView: 4, spaceBetween: 50 },
              }}
            >
              {/* 所有SwiperSlide内容保持不变 */}
              <SwiperSlide>
                <div data-aos="fade-left" className="mt-[80px] md:mx-5 mb-[45px]">
                  <div className="border-black mb-[40px] border-b-[1px] ">
                    <h3 className="font-bold py-1">Lisa A.</h3>
                  </div>
                  <div className="mb-[40px]">
                    <p>
                      “I consider myself a centrist - all the other news sources
                      lean so far to the left or right, it makes my head spin.
                      Thank you for{" "}
                      <span className={`bg-[#6fef8d]`}>
                        providing strictly the facts
                      </span>{" "}
                      - you re the only news source that can actually pull off
                      neutrality these days.”
                    </p>
                  </div>
                </div>
              </SwiperSlide>
              <SwiperSlide>
                <div data-aos="fade-left" className="mt-[80px] md:mx-2 mb-[45px]">
                  <div className="border-black mb-[40px] border-b-[1px] ">
                    <h3 className="font-bold py-1">Emma S.</h3>
                  </div>
                  <div className="mb-[40px]">
                    <p>
                      “I consider myself a centrist - all the other news sources
                      lean so far to the left or right, it makes my head spin.
                      Thank you for{" "}
                      <span className={`bg-[#EFEF6F]`}>
                        providing strictly the facts
                      </span>{" "}
                      - you re the only news source that can actually pull off
                      neutrality these days.”
                    </p>
                  </div>
                </div>
              </SwiperSlide>
              <SwiperSlide>
                <div data-aos="fade-left" className="mt-[80px] md:mx-5 mb-[45px]">
                  <div className="border-black mb-[40px] border-b-[1px] ">
                    <h3 className="font-bold py-1">Jim M.</h3>
                  </div>
                  <div className="mb-[40px]">
                    <p>
                      “I stopped watching the news, so sick of the bias. Was
                      searching for an alternative that would just tell me WHAT
                      happened, with NO editorializing. I found it. It s called
                      1440. It assumes you are smart enough to{" "}
                      <span className={`bg-[#7EDCF2]`}>
                        form your own opinions.
                      </span>
                    </p>
                  </div>
                </div>
              </SwiperSlide>
              <SwiperSlide>
                <div data-aos="fade-left" className="mt-[80px] md:mx-5 mb-[45px]">
                  <div className="border-black mb-[40px] border-b-[1px] ">
                    <h3 className="font-bold py-1">Zach N.</h3>
                  </div>
                  <div className="mb-[40px]">
                    <p className="">
                      “I stopped my habit of spending my day doomscrolling. I
                      signed up for 1440 &amp;{" "}
                      <span className={`bg-[#F48CD9]`}>
                        feel better informed in less time.
                      </span>{" "}
                      I m also less stressed out. Thank you.
                    </p>
                  </div>
                </div>
              </SwiperSlide>
            </Swiper>
          </React.Suspense>
        )}
      </div>
    </div>
  );
}

export default Testimonials;

方法2:临时关闭loop配置(业务允许时)

如果不需要循环轮播功能,直接将loop: true改为loop: false,让服务端与客户端的初始 Slide 保持一致,即可避免内容不匹配问题。

方法3:Next.js 专属优化

若使用 Next.js,可直接用next/dynamic禁用服务端渲染:

import dynamic from 'next/dynamic';
const Swiper = dynamic(() => import('swiper/react').then(mod => mod.Swiper), { ssr: false });
const SwiperSlide = dynamic(() => import('swiper/react').then(mod => mod.SwiperSlide), { ssr: false });

额外优化点

修复了 AOS 的useEffect逻辑,添加空依赖数组[],避免组件每次渲染都重复初始化 AOS,提升页面性能。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 22:27:20