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

Next.js项目Hydration Error问题排查求助

解决Next.js Hydration Error问题

核心问题定位

  1. page.tsx中map误用异步函数
    Home组件里result.posts.map传入了async函数,这会让map返回Promise数组而非React元素数组。服务端渲染时会把Promise转成无效HTML,客户端hydrate时完全匹配不上,直接触发Hydration Error。

  2. LikeButton初始状态与服务端渲染不一致
    LikeButton是客户端组件,初始isLiked设为false,但服务端渲染时不会执行客户端的useEffect请求点赞状态,导致服务端输出灰色心形图,客户端hydrate后立刻请求接口更新为实际状态(可能是已点赞的填充心形),前后HTML不匹配触发错误。

具体修复步骤

1. 移除map中的async函数

修改page.tsx里的map逻辑,直接删掉async:

{result.posts.map((post, index) => {
  return (
    <ThreadCard
      key={post._id}
      id={post._id}
      loggedInUser={user?.id || ""}
      currentUserId={userInfo._id || ""}
      parentId={post.parentId}
      content={post.text}
      author={post.author}
      community={post.community}
      createdAt={post.createdAt}
      comments={post.children}
    />
  );
})}

原代码里的async完全多余,因为map内部没用到await,直接移除就行。

2. 预取点赞状态,避免客户端初始状态偏差

既然ThreadCard在服务端渲染,直接在服务端获取每个帖子的点赞状态,传给LikeButton,不用让客户端单独请求:

步骤1:扩展fetchPosts,预取每个帖子的点赞状态

在fetchPosts函数里,给每个post加上isLiked字段,用当前登录用户ID判断是否已点赞:

async function fetchPosts(page: number, limit: number, userId: string) {
  const posts = await // 原获取帖子的逻辑
  // 为每个帖子添加点赞状态
  const postsWithLikeStatus = await Promise.all(
    posts.map(async post => {
      const isLiked = await checkIfUserLikedPost(post._id, userId);
      return {...post, isLiked, likesCount: post.likes.length};
    })
  );
  return {posts: postsWithLikeStatus, isNext: ...};
}

步骤2:在page.tsx中传递用户ID给fetchPosts

const result = await fetchPosts(
  searchParams.page ? +searchParams.page : 1,
  30,
  userInfo._id // 传入当前用户ID
);

步骤3:ThreadCard接收点赞状态并传给LikeButton

修改ThreadCard的Props:

interface Props {
  // ...其他属性
  isLiked: boolean;
  likesCount: number; // 替换硬编码的1,用帖子实际点赞数
}

在ThreadCard里用传入的参数:

const ThreadCard = ({
  // ...其他props
  isLiked,
  likesCount,
}: Props) => {
  // 删掉const likesCount = 1;
  // ...其余代码
  // 给LikeButton传入初始点赞状态
  <LikeButton
    threadId={id.toString()}
    userId={currentUserId.toString()}
    initialIsLiked={isLiked}
  />
};

步骤4:修改LikeButton使用初始状态

interface Props {
  threadId: string;
  userId: string;
  initialIsLiked: boolean; // 新增初始状态属性
}

const LikeButton = ({ threadId, userId, initialIsLiked }: Props) => {
  const [isLiked, setIsLiked] = useState<boolean>(initialIsLiked); // 用服务端传的初始状态
  // ...其余代码
  // 可以保留useEffect里的fetch请求,用于同步服务端缓存过期的情况
};

3. 临时方案:延迟渲染LikeButton(无法预取状态时用)

如果暂时没法在服务端预取点赞状态,让LikeButton等客户端hydrate完成后再渲染,避免初始状态不匹配:

const LikeButton = ({ threadId, userId }: Props) => {
  const [isLiked, setIsLiked] = useState<boolean>(false);
  const [isClient, setIsClient] = useState(false); // 标记是否已在客户端

  useEffect(() => {
    setIsClient(true); // 客户端渲染完成后切换状态
    // ...原fetchInitialLikeStatus和setupLikeFunctions逻辑
  }, [threadId, userId]);

  // 客户端未准备好时,渲染和服务端一致的占位图
  if (!isClient) {
    return (
      <Image
        src="/assets/heart-gray.svg"
        alt="heart"
        width={24}
        height={24}
        className="cursor-pointer object-contain"
      />
    );
  }

  return (
    <Image
      src={isLiked ? "/assets/heart-filled.svg" : "/assets/heart-gray.svg"}
      alt="heart"
      width={24}
      height={24}
      className="cursor-pointer object-contain"
      onClick={handleLike}
    />
  );
};

额外检查点

  • 把ThreadCard里硬编码的likesCount: 1改成从post数据里取实际点赞数,避免服务端和客户端显示数字不一致。
  • 检查DeleteThread组件是否也有依赖客户端状态的逻辑,导致渲染不一致。

内容的提问来源于stack exchange,提问作者Andika Wahyu Syaputra

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 23:57:03