Next.js 14中客户端组件Props两次Hydration导致初始值为空的问题求助
兄弟我太懂你这问题有多闹心了!我之前在Next.js 14的App Router里踩过几乎一模一样的坑,先帮你捋清楚问题根源,再给你几个实打实的解决思路:
先排查最容易踩的基础错误
看你贴的代码,有两个非常关键的小问题,大概率是导致你出现undefined的元凶:
1. 服务器组件的fetch逻辑写错了
你的Page组件里,const data = awaits fetch(dataUrl) 有两处错误:
- 是
await不是awaits - 没有把fetch的响应解析成JSON,直接把Response对象传给了客户端组件!
正确的服务器组件代码应该是这样:
'use server'; // 服务器组件必须是async函数! const Page = async () => { const res = await fetch(dataUrl); if (!res.ok) { throw new Error('Failed to fetch data'); } // 把响应转换成JSON格式的普通对象 const data = await res.json(); return ( <> <Comp1 data={data} /> <Comp2 data={data} /> </> ) }
如果不解析成JSON,客户端组件拿到的是Response对象,自然解构不出header和hero,会出现undefined。
2. 客户端组件的props接收方式错误
你写的const Comp2 = (data) => { ... }是错的!React组件的props是一个对象,你需要从props对象里解构出data字段:
'use client'; // 这里要解构props,把data从props对象里取出来 const Comp2 = ({ data = {} }) => { // 给data设默认空对象,避免初始时undefined报错 console.log("data", data) const [hero, setHero] = useState('') useEffect(() => { // 加可选链,防止data为undefined时访问hero报错 if (data?.hero) { setHero(`${data.hero} - hero`) } }, [data]) // 现在依赖的是正确的data对象 return ( <div> {/* 用可选链处理data可能为空的情况 */} <h1>{data?.header}</h1> <DataHero hero={hero} /> </div> ) }
之前你直接把data当成props参数,其实拿到的是整个props对象,不是你传的那个数据,这肯定会出现undefined!
解决Hydration顺序导致的控制台输出反转问题
你提到控制台先输出正确数据,再输出undefined,这是Next.js Hydration的特性:服务器先渲染HTML并发送到客户端,客户端Hydration时会先复用服务器的HTML,然后再重新渲染一次客户端组件,这就导致了两次console.log。
要解决这个问题,你可以在客户端组件里判断是否是Hydration完成后的客户端渲染:
'use client'; import { useEffect, useState, useRef } from 'react'; const Comp2 = ({ data = {} }) => { const isHydrated = useRef(false); const [hero, setHero] = useState('') useEffect(() => { isHydrated.current = true; }, []); useEffect(() => { // 只在Hydration完成后更新状态 if (isHydrated.current && data?.hero) { setHero(`${data.hero} - hero`) } }, [data]) return ( <div> <h1>{data?.header}</h1> <DataHero hero={hero} /> </div> ) }
通过useRef标记Hydration完成,避免在Hydration阶段不必要的状态更新,也能让控制台的输出更符合预期。
其他你试过的方法为什么没用?
JSON.parse(JSON.stringify(data)):这个方法本身是把对象转成可序列化的普通对象,但你之前没解决fetch和props解构的问题,所以没用;现在修复基础问题后,这个方法可以用来确保data是纯对象,避免一些Hydration的引用问题。- 懒加载+Suspense:如果要用这个方法,需要在服务器组件里懒加载客户端组件,并包裹Suspense:
'use server'; import { Suspense, lazy } from 'react'; const Comp2 = lazy(() => import('./Comp2')); const Page = async () => { // ... fetch data逻辑 return ( <> <Comp1 data={data} /> <Suspense fallback={<div>Loading...</div>}> <Comp2 data={data} /> </Suspense> </> ) }
这样可以让客户端组件在数据准备好后再渲染,避免初始空状态,但前提还是要先修复前面的基础错误。
你先按上面的步骤逐一排查,最可能的就是服务器组件没解析JSON或者客户端组件没解构props这两个问题。我之前就是因为没把fetch响应转成JSON,折腾了半天!
内容来源于stack exchange

