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

NextJS条件渲染遇Hydration失败问题求助

NextJS Hydration失败问题解决方案(条件渲染购物车场景)

问题描述

我是NextJS新手,在通过条件渲染展示购物车数据时触发Hydration失败错误。加载token前页面显示“无购物车”,token加载完成后数据更新,页面就报错;多个通过Redux或API获取数据后更新内容的页面都存在这个问题。

错误提示:

Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client.

相关代码:

<div className="w-full">
  <div className="overflow-x-auto">
    <table className="w-full border-collapse bg-white shadow-md rounded-lg">
      <thead>
        <tr className="bg-gray-100">
          <th className="p-3 text-left text-black">Image</th>
          <th className="p-3 text-left text-black">Product</th>
          <th className="p-3 text-left text-black">Price</th>
          <th className="p-3 text-left text-black">Size</th>
          <th className="p-3 text-left text-black">Quantity</th>
          <th className="p-3 text-left text-black">Total</th>
          <th className="p-3 text-left text-black">Action</th>
        </tr>
      </thead>
      <tbody>
        {reduxCart.length > 0 ? (
          reduxCart.map((item) =>
            item.quantity > 0 ? (
              <tr key={item.productId} className="border-b">
                <td className="p-3">
                  {item.image ? (
                    <Image
                      src={item.image}
                      alt={item.name}
                      className="w-16 h-16 object-cover rounded-md"
                      width={64}
                      height={64}
                    />
                  ) : (
                    <span>No image available</span>
                  )}
                </td>
                <td className="p-3 text-black">{item.name}</td>
                <td className="p-3 text-black">₹{item.price}</td>
                <td className="p-3 text-black">
                  <select
                    value={item.size}
                    onChange={(e) => dispatch(updateSize({ productId: item.productId, size: e.target.value }))}
                    className="border p-2 rounded-md"
                  >
                    <option value="">Select Size</option>
                    <option value="S">S</option>
                    <option value="M">M</option>
                    <option value="L">L</option>
                    <option value="XL">XL</option>
                    <option value="XXL">XXL</option>
                  </select>
                </td>
                <td className="p-3 text-black flex items-center">
                  <Button onClick={() => dispatch(decreaseQuantity(item.productId, item.size))} className="bg-green-500 px-2 py-1 rounded">
                    -
                  </Button>
                  <span className="mx-2">{item.quantity}</span>
                  <Button onClick={() => dispatch(increaseQuantity(item.productId, item.size))} className="bg-green-500 px-2 py-1 rounded">
                    +
                  </Button>
                </td>
                <td className="p-3 text-black">₹{item.price * item.quantity}</td>
                <td className="p-3 text-left">
                  <Button onClick={() => dispatch(removeFromCart(item.productId, item.size))} className="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded-md">
                    Remove
                  </Button>
                </td>
              </tr>
            ) : null
          )
        ) : (
          <tr>
            <td colSpan="7" className="text-center p-3 text-black">No items in cart</td>
          </tr>
        )}
      </tbody>
    </table>
  </div>
</div>

核心原因

服务端渲染时reduxCart是空数组,渲染了“No items in cart”;但客户端初始化后,Redux获取到购物车数据,reduxCart变为非空,DOM结构瞬间变化,导致服务端和客户端的HTML不匹配,触发Hydration失败。

解决方案

1. 延迟客户端渲染,避免初始DOM不匹配

通过useEffect设置客户端挂载标记,服务端渲染时先展示和最终结构一致的加载状态,客户端挂载后再渲染实际购物车内容:

import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Image from 'next/image';
import Button from './Button'; // 假设Button是你的自定义组件

function Cart() {
  const [isClient, setIsClient] = useState(false);
  const reduxCart = useSelector(state => state.cart.items);
  const dispatch = useDispatch();

  useEffect(() => {
    setIsClient(true);
  }, []);

  // 服务端渲染时保持DOM结构和客户端一致,避免不匹配
  if (!isClient) {
    return (
      <div className="w-full">
        <div className="overflow-x-auto">
          <table className="w-full border-collapse bg-white shadow-md rounded-lg">
            <thead>
              <tr className="bg-gray-100">
                <th className="p-3 text-left text-black">Image</th>
                <th className="p-3 text-left text-black">Product</th>
                <th className="p-3 text-left text-black">Price</th>
                <th className="p-3 text-left text-black">Size</th>
                <th className="p-3 text-left text-black">Quantity</th>
                <th className="p-3 text-left text-black">Total</th>
                <th className="p-3 text-left text-black">Action</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td colSpan="7" className="text-center p-3 text-black">Loading...</td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    );
  }

  // 客户端挂载后渲染实际内容
  return (
    <div className="w-full">
      <div className="overflow-x-auto">
        <table className="w-full border-collapse bg-white shadow-md rounded-lg">
          <thead>
            <tr className="bg-gray-100">
              <th className="p-3 text-left text-black">Image</th>
              <th className="p-3 text-left text-black">Product</th>
              <th className="p-3 text-left text-black">Price</th>
              <th className="p-3 text-left text-black">Size</th>
              <th className="p-3 text-left text-black">Quantity</th>
              <th className="p-3 text-left text-black">Total</th>
              <th className="p-3 text-left text-black">Action</th>
            </tr>
          </thead>
          <tbody>
            {reduxCart.length > 0 ? (
              reduxCart.map((item) =>
                item.quantity > 0 ? (
                  <tr key={item.productId} className="border-b">
                    <td className="p-3">
                      {item.image ? (
                        <Image
                          src={item.image}
                          alt={item.name}
                          className="w-16 h-16 object-cover rounded-md"
                          width={64}
                          height={64}
                        />
                      ) : (
                        <span>No image available</span>
                      )}
                    </td>
                    <td className="p-3 text-black">{item.name}</td>
                    <td className="p-3 text-black">₹{item.price}</td>
                    <td className="p-3 text-black">
                      <select
                        value={item.size}
                        onChange={(e) => dispatch(updateSize({ productId: item.productId, size: e.target.value }))}
                        className="border p-2 rounded-md"
                      >
                        <option value="">Select Size</option>
                        <option value="S">S</option>
                        <option value="M">M</option>
                        <option value="L">L</option>
                        <option value="XL">XL</option>
                        <option value="XXL">XXL</option>
                      </select>
                    </td>
                    <td className="p-3 text-black flex items-center">
                      <Button onClick={() => dispatch(decreaseQuantity(item.productId, item.size))} className="bg-green-500 px-2 py-1 rounded">
                        -
                      </Button>
                      <span className="mx-2">{item.quantity}</span>
                      <Button onClick={() => dispatch(increaseQuantity(item.productId, item.size))} className="bg-green-500 px-2 py-1 rounded">
                        +
                      </Button>
                    </td>
                    <td className="p-3 text-black">₹{item.price * item.quantity}</td>
                    <td className="p-3 text-left">
                      <Button onClick={() => dispatch(removeFromCart(item.productId, item.size))} className="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded-md">
                        Remove
                      </Button>
                    </td>
                  </tr>
                ) : null
              )
            ) : (
              <tr>
                <td colSpan="7" className="text-center p-3 text-black">No items in cart</td>
              </tr>
            )}
          </tbody>
        </table>
      </div>
    </div>
  );
}

export default Cart;

2. 服务端预取购物车数据(SSR场景)

如果页面使用服务端渲染(SSR),可以在getServerSideProps中获取用户token并拉取购物车数据,确保服务端和客户端初始渲染内容一致:

import { useSelector, useDispatch } from 'react-redux';
import { setCart } from '../redux/cartSlice';

export async function getServerSideProps(context) {
  // 从请求cookie中获取token
  const token = context.req.cookies?.token;
  let initialCart = [];

  if (token) {
    // 调用API获取购物车数据
    const res = await fetch('https://your-api-domain/cart', {
      headers: { Authorization: `Bearer ${token}` }
    });
    initialCart = await res.json();
  }

  return {
    props: { initialCart }
  };
}

function Cart({ initialCart }) {
  const dispatch = useDispatch();
  const reduxCart = useSelector(state => state.cart.items);

  useEffect(() => {
    // 初始化Redux购物车数据
    dispatch(setCart(initialCart));
  }, [dispatch, initialCart]);

  // 正常渲染购物车内容(原代码结构)
  return (
    <div className="w-full">
      {/* ...原代码内容... */}
    </div>
  );
}

export default Cart;

3. 禁用组件SSR(动态导入)

如果购物车组件不需要服务端渲染,用NextJS的dynamic导入并禁用SSR,确保只在客户端渲染:

import dynamic from 'next/dynamic';

// 动态导入Cart组件,禁用SSR
const Cart = dynamic(() => import('../components/Cart'), { ssr: false });

function CartPage() {
  return <Cart />;
}

export default CartPage;

关键注意点

  • 服务端渲染时,组件初始状态必须和客户端初始化后的状态尽可能一致,避免DOM结构突变
  • 涉及用户私有数据的场景,要么服务端预取数据,要么延迟客户端渲染直到数据准备完毕
  • 不要在服务端渲染依赖客户端环境的内容(如localStorage、window对象)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 02:11:00