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

Next.js中React Hydration错误求助:服务端HTML缺少匹配Button

问题描述

在Next.js项目中使用@nextui-org/react的自定义分页和卡片组件时,本地运行正常,但部署到生产环境后出现Hydration失败错误:

Hydration failed because the initial UI does not match what was rendered on the server.

同时伴随DOM匹配警告:

Expected server HTML to contain a matching <button> in <div>.

相关组件代码如下:

import React, { useState } from "react";
import { Pagination as NextUIPagination } from "@nextui-org/react";
import { Card as NextUICard, CardHeader, CardFooter, Image, Button } from "@nextui-org/react";

// Interface for Pagination component props
interface PaginationProps {
    total: number;
    currentPage: number;
    onPageChange: (page: number) => void;
}

// Pagination component
const Pagination: React.FC<PaginationProps> = ({ total, currentPage, onPageChange }) => {
    return (
        <NextUIPagination
            key="pagination"
            showControls
            total={total}
            initialPage={currentPage}
            onChange={onPageChange}
            aria-label="Pagination Navigation"
        />
    );
};

// Interface for Card component props
interface CardProps {
    location: string;
    description: string;
    price: string;
    rating: string;
    image: string;
    href: string;
}

// Card component
const Card: React.FC<CardProps> = ({ location, description, price, rating, image, href }) => {
    const [svgColor, setSvgColor] = useState<string | undefined>(undefined);

    const toggleSvgColorChange = () => {
        setSvgColor((prevColor) => (prevColor === 'red' ? undefined : 'red'));
    };

    return (
        <>
            <NextUICard
                key={href}
                isPressable
                radius="lg"
                className="border-none bg-opacity-0 backdrop-blur-lg"
                shadow="md"
            >
                <Image
                    isBlurred
                    isZoomed
                    alt={description}
                    src={image}
                    className="object-cover"
                />
                <CardHeader className="flex justify-end overflow-hidden py-1 absolute top-2 w-[calc(100%_-_8px)]">
                    <Button
                        isIconOnly
                        variant="light"
                        color="default"
                        size="sm"
                        className="before:bg-white/10 border-white/20 border-1 before:rounded-xl shadow-small"
                        onClick={toggleSvgColorChange}
                        aria-label="Favorite"
                    >
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill={svgColor || "currentColor"}
                            className="w-5 h-5 text-default-500">
                            <path
                                d="m9.653 16.915-.005-.003-.019-.01a20.759 20.759 0 0 1-1.162-.682 22.045 22.045 0 0 1-2.582-1.9C4.045 12.733 2 10.352 2 7.5a4.5 4.5 0 0 1 8-2.828A4.5 4.5 0 0 1 18 7.5c0 2.852-2.044 5.233-3.885 6.82a22.049 22.049 0 0 1-3.744 2.582l-.019.01-.005.003h-.002a.739.739 0 0 1-.69.001l-.002-.001Z"/>
                        </svg>
                    </Button>
                </CardHeader>
                <CardFooter className="gap-1.5 flex-col items-start">
                    <div className="w-full flex justify-between">
                        <p key="location">{location}</p>
                        <div className="flex flex-row gap-1.5 items-center">
                            <svg
                                xmlns="http://www.w3.org/2000/svg"
                                viewBox="0 0 20 20"
                                fill="currentColor"
                                className="w-4 h-4 text-default-400">
                                <path
                                    fillRule="evenodd"
                                    d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.107-.536 1.651l3.62 3.102-1.106 4.637c-.194.813.691 1.456 1.405 1.02L10 15.591l4.069 2.485c.713.436 1.598-.207 1.404-1.02l-1.106-4.637 3.62-3.102c.635-.544.297-1.584-.536-1.65l-4.752-.382-1.831-4.401Z"
                                    clipRule="evenodd"/>
                            </svg>
                            <p className="text-default-400 text-sm" key="rating">{rating}</p>
                        </div>
                    </div>
                    <p className="text-sm text-default-500 text-left" key="description">{description}</p>
                    <p className="text-sm text-default-400" key="price">{price}</p>
                </CardFooter>
            </NextUICard>
        </>
    );
};

export { Pagination, Card };
解决方案

问题核心是服务端渲染的DOM结构与客户端Hydration后的DOM结构不一致,以下是针对性修复方案:

1. 延迟客户端状态初始化

Card组件中useState初始化的svgColor会导致服务端与客户端渲染差异——服务端无状态,客户端Hydration时会立即应用初始状态,和NextUI服务端渲染的输出冲突。

修复:用useEffect将状态初始化延迟到客户端挂载后:

import React, { useState, useEffect } from "react";

// 在Card组件内部修改状态逻辑
const Card: React.FC<CardProps> = ({ ...props }) => {
    const [svgColor, setSvgColor] = useState<string | undefined>(undefined);

    // 仅在客户端挂载后初始化状态
    useEffect(() => {
        setSvgColor(undefined); // 保持原初始值,仅延迟到客户端执行
    }, []);

    // ... 其余代码不变
};

2. 禁用组件的SSR渲染

如果NextUI组件本身存在SSR兼容性问题,可使用Next.js的dynamic导入组件并禁用SSR:

import dynamic from 'next/dynamic';

// 动态导入组件,禁用服务端渲染
const Pagination = dynamic(() => import('./path-to-your-component').then(mod => mod.Pagination), { ssr: false });
const Card = dynamic(() => import('./path-to-your-component').then(mod => mod.Card), { ssr: false });

3. 移除多余的Fragment包裹

Card组件外层的空Fragment(<>...</>)可能导致DOM结构的细微差异,直接返回NextUICard即可:

// 修改前
return (
    <>
        <NextUICard ...>...</NextUICard>
    </>
);

// 修改后
return (
    <NextUICard ...>...</NextUICard>
);

4. 确保Props的一致性

检查传入组件的所有props(如location、image等)在服务端和客户端是否完全一致,避免因数据差异导致UI渲染不同。

5. 更新NextUI版本

确保使用的@nextui-org/react是最新版本,新版本通常会修复SSR相关的DOM匹配问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 12:25:55