NextJS静态网站Hydration失败求助:服务端与客户端HTML不匹配
遇到的错误
Error: Hydration failed because the initial UI does not match what was rendered on the server
Warning: Expected server HTML to contain a matching<img>in<div>.
问题代码
const JiuCafe = () => { const thumbnail = [ { id: 1, img: "https://image.img", }, ]; const detail = [ { name: "name", img: "https://image.img", description: "description", }, ]; return ( <div className="shadow"> <section id="services" className="services section-bg"> <div className="container-fluid"> <div className="row row-sm"> <div className="col-md-6 _boxzoom"> <div className="zoom-thumb"> <ul className="piclist"> {thumbnail.map(({ img }) => ( <li> <img src={img} alt=""></img> </li> ))} </ul> </div> <div className="_product-images"> <div className="picZoomer"> {detail.map(({ img }) => ( <img className="my_img" src={img} alt=""></img> ))} </div> </div> </div> <div className="col-md-6"> <div className="_product-detail-content"> {detail.map(({ name, description }) => ( <li> <p className="_p-name"> <b>{name}</b> </p> <div className="_p-price-box"> <div className="_p-features"> <span> {description} </span> </div> </div> </li> ))} </div> </div> </div> </div> </section> </div> ); };
问题分析与解决方法
这个Hydration不匹配问题的核心是服务端渲染的HTML和客户端首次渲染的DOM结构不一致,结合代码来看,需要修正以下几个点:
1. 给循环渲染的元素添加唯一key属性
使用map循环渲染元素时,必须给每个子元素添加唯一key,Next.js依赖这个属性匹配服务端和客户端的元素,缺失key极易导致DOM结构不匹配。
修改所有map循环的元素:
- 缩略图列表:
{thumbnail.map(({ id, img }) => ( <li key={id}> <img src={img} alt="" /> </li> ))}
- 主图部分:
{detail.map((item, index) => ( <img key={index} className="my_img" src={item.img} alt="" /> ))}
- 详情列表:
{detail.map((item, index) => ( <li key={index}> <p className="_p-name"> <b>{item.name}</b> </p> <div className="_p-price-box"> <div className="_p-features"> <span> {item.description} </span> </div> </div> </li> ))}
(如果detail数组有唯一id,用id当key效果更好)
2. 修正JSX中img标签的写法
HTML写法的<img src={img} alt=""></img>在JSX里应改为自闭合标签<img src={img} alt="" />,虽然浏览器能兼容,但Next.js服务端渲染时可能因标签格式差异触发Hydration错误。
3. 用Next.js内置Image组件替换原生img
Next.js的Image组件会自动优化图片加载逻辑,避免服务端与客户端的渲染差异。首先导入组件:
import Image from 'next/image';
然后替换所有img标签:
- 缩略图:
<Image src={img} alt="" width={80} height={80} />
- 主图:
<div className="picZoomer" style={{ position: 'relative', width: '100%', height: '400px' }}> {detail.map((item, index) => ( <Image key={index} className="my_img" src={item.img} alt="" fill objectFit="contain" /> ))} </div>
(fill属性需要父容器设置position: relative,这里直接给.picZoomer加了行内样式)
4. 修复语义化DOM结构
_product-detail-content里直接渲染<li>但没有包裹<ul>/<ol>,不符合HTML语义,也可能导致DOM解析差异,给详情列表加上父容器:
<div className="_product-detail-content"> <ul> {detail.map((item, index) => ( <li key={index}> {/* 原有内容不变 */} </li> ))} </ul> </div>
完整修正后的代码示例
import Image from 'next/image'; const JiuCafe = () => { const thumbnail = [ { id: 1, img: "https://image.img", }, ]; const detail = [ { name: "name", img: "https://image.img", description: "description", }, ]; return ( <div className="shadow"> <section id="services" className="services section-bg"> <div className="container-fluid"> <div className="row row-sm"> <div className="col-md-6 _boxzoom"> <div className="zoom-thumb"> <ul className="piclist"> {thumbnail.map(({ id, img }) => ( <li key={id}> <Image src={img} alt="" width={80} height={80} /> </li> ))} </ul> </div> <div className="_product-images"> <div className="picZoomer" style={{ position: 'relative', width: '100%', height: '400px' }}> {detail.map((item, index) => ( <Image key={index} className="my_img" src={item.img} alt="" fill objectFit="contain" /> ))} </div> </div> </div> <div className="col-md-6"> <div className="_product-detail-content"> <ul> {detail.map((item, index) => ( <li key={index}> <p className="_p-name"> <b>{item.name}</b> </p> <div className="_p-price-box"> <div className="_p-features"> <span> {item.description} </span> </div> </div> </li> ))} </ul> </div> </div> </div> </div> </section> </div> ); }; export default JiuCafe;
内容的提问来源于stack exchange,提问作者Clement Chow

