Next.js 13.4 Hydration错误求助:服务端与客户端UI不匹配
Next.js 13.4 Hydration失败问题:服务端渲染UI与初始客户端UI不匹配
错误详情
未处理的运行时错误
错误:Hydration失败,因为初始UI与服务端渲染的内容不匹配。警告:预期服务端HTML的<ul>中包含对应的<li>。
组件栈
- li
- ul
- div
- Todos(路径:./src/components/Todos.tsx (15:118))
- div
组件代码
"use client" import {useTodos} from "@/store/todos"; import { useSearchParams } from "next/navigation"; export const Todos = () => { const {todos, toggleTodoAsCompleted, handleDeleteTodo} = useTodos(); const searchParams = useSearchParams(); const todosFilter = searchParams.get('todos') console.log("params " + todosFilter) let filteredTodos = todos; if (todosFilter === "active") { filteredTodos = todos.filter((todo) => !todo.completed); } else if (todosFilter === "completed") { filteredTodos = todos.filter((todo) => todo.completed); } return ( <div> <p>List of todos</p> <ul className="main-task"> {filteredTodos.map((todo) => ( <li key={todo.id}> <input type="checkbox" id={`todo-${todo.id}`} checked={todo.completed} onChange={() => { console.log(todo.completed); toggleTodoAsCompleted(todo.id); }} /> <label htmlFor={`todo-${todo.id}`}>{todo.task}</label> {todo.completed && ( <button type="button" onClick={() => handleDeleteTodo(todo.id)}> Delete </button> )} </li> ))} </ul> </div> ) }
已尝试的解决方法
- 将
<ul>和<li>改为<div>和<section> - 查阅官方文档中Hydration错误的常见原因,但未解决问题
官方列出的Hydration错误常见原因
- HTML标签嵌套错误
<p>嵌套在另一个<p>标签中<div>嵌套在<p>标签中- 交互式内容无法嵌套(如
<a>嵌套在<a>标签中、<button>嵌套在<button>标签中等)
- 在渲染逻辑中使用
typeof window !== 'undefined'这类检查 - 在渲染逻辑中使用仅浏览器端可用的API,如
window或localStorage - 浏览器扩展修改了HTML
- CSS-in-JS库配置错误
- 确保代码遵循官方示例
- Edge/CDN配置错误,尝试修改HTML响应,如Cloudflare的自动压缩
解决方案
问题根源:useSearchParams在服务端渲染阶段无法获取到请求的查询参数,导致服务端渲染的列表内容与客户端hydrate时根据查询参数过滤后的内容不一致,触发Hydration不匹配。
修改代码,通过useEffect延迟处理查询参数,确保服务端先渲染完整列表,客户端完成hydrate后再更新过滤结果:
"use client" import { useTodos } from "@/store/todos"; import { useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; export const Todos = () => { const { todos, toggleTodoAsCompleted, handleDeleteTodo } = useTodos(); const searchParams = useSearchParams(); const [filteredTodos, setFilteredTodos] = useState(todos); useEffect(() => { const todosFilter = searchParams.get('todos'); console.log("params " + todosFilter); if (todosFilter === "active") { setFilteredTodos(todos.filter((todo) => !todo.completed)); } else if (todosFilter === "completed") { setFilteredTodos(todos.filter((todo) => todo.completed)); } else { setFilteredTodos(todos); } }, [searchParams, todos]); return ( <div> <p>List of todos</p> <ul className="main-task"> {filteredTodos.map((todo) => ( <li key={todo.id}> <input type="checkbox" id={`todo-${todo.id}`} checked={todo.completed} onChange={() => { console.log(todo.completed); toggleTodoAsCompleted(todo.id); }} /> <label htmlFor={`todo-${todo.id}`}>{todo.task}</label> {todo.completed && ( <button type="button" onClick={() => handleDeleteTodo(todo.id)}> Delete </button> )} </li> ))} </ul> </div> ) }
说明
useEffect会在客户端组件完成hydrate后执行,此时useSearchParams能正确获取到查询参数,再根据参数更新过滤后的列表,保证服务端渲染的初始内容与客户端hydrate时的内容一致,解决Hydration不匹配问题。
内容的提问来源于stack exchange,提问作者Pranshu Sati
相关产品推荐
相关产品推荐

