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

Next.js 13 AppDir中解决"Objects are not valid as a React child"报错

问题根源

你遇到的错误是因为带"use client"标记的客户端组件不能定义为async函数。在Next.js 13 App Router中,async组件会返回Promise对象,而React无法直接渲染Promise,这就触发了"Objects are not valid as a React child"错误。你的Post组件同时满足两个条件:

  • 标记了"use client"(因为要使用react-hook-form的useForm钩子,必须在客户端运行)
  • 是async函数(用来从Sanity获取文章数据)

导致组件本身被当作Promise渲染,从而报错。

解决方案(推荐方案)

拆分组件,用Server Component负责数据获取,客户端组件负责UI和表单逻辑,符合Next.js 13的最佳实践:

步骤1:创建客户端组件PostClient.tsx

把原组件中需要客户端运行的部分(表单、useForm钩子)提取到单独的客户端组件中:

"use client";

import Image from "next/image";
import urlFor from "../../../../lib/urlFor";
import { PortableText } from "@portabletext/react";
import { RichTextComponents } from "../../../../components/RichTextComponents";
import { useForm, SubmitHandler } from "react-hook-form";

interface IFormInput {
  _id: string;
  name: string;
  email: string;
  comment: string;
}

interface PostClientProps {
  post: Post;
}

const PostClient = ({ post }: PostClientProps) => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<IFormInput>();

  const onSubmit: SubmitHandler<IFormInput> = async (data) => {
    await fetch("/api/createComment", {
      method: "POST",
      body: JSON.stringify(data),
    })
      .then(() => console.log(data))
      .catch((err) => console.log(err));
  };

  return (
    <>
      <article className="px-10 pb-28 mt-10">
        <section className="space-y-2 border border-[#f7ab0a] text-white">
          <div className="relative min-h-56 flex flex-col md:flex-row justify-between">
            <div className="absolute top-0 w-full h-full opacity-10 blur-sm p-10">
              <Image
                className="object-cover object-center mx-auto"
                src={urlFor(post.mainImage).url()}
                alt={post.author.name}
                fill
              />
            </div>
            <section className="p-5 bg-[#f7ab0a] w-full">
              <div className="flex flex-col md:flex-row justify-between gap-y-5">
                <div>
                  <h1 className="text-4xl font-extrabold">{post.title}</h1>
                  <p>
                    {new Date(post._createdAt).toLocaleDateString("en-US", {
                      day: "numeric",
                      month: "long",
                      year: "numeric",
                    })}
                  </p>
                </div>
                <div className="flex items-center space-x-2">
                  <Image
                    className="rounded-full"
                    src={urlFor(post.author.image).url()}
                    alt={post.author.name}
                    height={40}
                    width={40}
                  />
                  <div className="w-64">
                    <h3 className="text-lg font-bold">{post.author.name}</h3>
                    <div></div>
                  </div>
                </div>
              </div>

              <div>
                <h2 className="italic pt-10">{post.description}</h2>
                <div className="flex items-center justify-end mt-auto space-x-2">
                  {post.categories.map((category) => (
                    <p
                      className="bg-gray-800 text-white px-3 py-1 rounded-full text-sm font-semibold mt-4"
                      key={category._id}
                    >
                      {category.title}
                    </p>
                  ))}
                </div>
              </div>
            </section>
          </div>
        </section>
        <PortableText value={post.body} components={RichTextComponents} />
      </article>
      <div>
        <hr className="max-w-lg my-5 mx-auto border border-yellow-500" />
        <form
          onSubmit={handleSubmit(onSubmit)}
          className="flex flex-col p-5 max-w-2xl mx-auto mb-10"
        >
          <h3 className="text-md text-yellow-500">Enjoyed this article?</h3>
          <h4 className="text-3xl font-bold">Leave a comment below!</h4>
          <hr className="py-3 mt-2" />

          <input
            {...register("_id")}
            type="hidden"
            name="_id"
            value={post._id}
          />
          <label className="block mb-5 ">
            <span className="text-gray-700">Name</span>
            <input
              {...register("name", { required: true })}
              className="shadow border rounded py-2 px-3 form-input mt-1 block w-full outline-none ring-yellow-500 focus:ring"
              placeholder="John Doe..."
              type="text"
            />
          </label>
          <label className="block mb-5 ">
            <span className="text-gray-700">Email</span>
            <input
              {...register("email", { required: true })}
              className="shadow border rounded py-2 px-3 form-input mt-1 block w-full outline-none ring-yellow-500 focus:ring"
              placeholder="example@gmail.com"
              type="email"
            />
          </label>
          <label className="block mb-5 ">
            <span className="text-gray-700">Comment</span>
            <textarea
              {...register("comment", { required: true })}
              className="shadow border rounded py-2 px-3 form-textarea mt-1 block w-full outline-none ring-yellow-500 focus:ring"
              placeholder="type your comments..."
              rows={8}
            />
          </label>

          <div className="flex flex-col p-5">
            {errors.name && (
              <span className="text-red-500">-- The name field is required</span>
            )}
            {errors.email && (
              <span className="text-red-500">-- The email field is required</span>
            )}
            {errors.comment && (
              <span className="text-red-500">-- The comment field is required</span>
            )}
          </div>

          <input
            type="submit"
            className="shadow bg-yellow-500 hover:bg-yellow-400 w-full focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded cursor-pointer"
          />
        </form>
      </div>
      <div className="relative">
        <a
          id="topp"
          rel="noreferrer"
          href="#top"
          className="back-to-top absolute bottom-4 right-0 rounded-3xl bg-[#f7ab0a] text-white hover:-translate-y-2 hover:bg-lime-600 hover:text-[#f7ab0a] p-2 text-2xl"
        >
          <i className="fas fa-angle-up " aria-hidden="true"></i>
        </a>
      </div>
    </>
  );
};

export default PostClient;

步骤2:修改原Post.tsx为Server Component

移除"use client"标记,保留数据获取和静态生成逻辑,然后渲染客户端组件并传递文章数据:

import { groq } from "next-sanity";
import { client } from "../../../../lib/sanity.client";
import PostClient from "./PostClient";

type Props = {
  params: {
    slug: string;
  };
};

export const revalidate = 30;

export async function generateStaticParams() {
  const query = groq`
    *[_type=="post"]
    {
      slug
    }
  `;
  const slugs: Post[] = await client.fetch(query);
  const slugRoutes = slugs.map((slug) => slug.slug.current);

  return slugRoutes.map((slug) => ({
    slug,
  }));
}

const Post = async ({ params: { slug } }: Props) => {
  const query = groq`
    *[_type=='post' && slug.current == $slug][0]
    {
        ...,
        author->,
        categories[]->
    }
  `;
  const post: Post = await client.fetch(query, { slug });

  return <PostClient post={post} />;
};

export default Post;
备选方案(客户端内获取数据)

如果不想拆分组件,可以在客户端组件中用useEffect获取数据,避免组件本身是async:

"use client";

import { useState, useEffect } from "react";
import { groq } from "next-sanity";
import Image from "next/image";
import { client } from "../../../../lib/sanity.client";
import urlFor from "../../../../lib/urlFor";
import { PortableText } from "@portabletext/react";
import { RichTextComponents } from "../../../../components/RichTextComponents";
import { useForm, SubmitHandler } from "react-hook-form";

interface IFormInput {
  _id: string;
  name: string;
  email: string;
  comment: string;
}

type Props = {
  params: {
    slug: string;
  };
};

const Post = ({ params: { slug } }: Props) => {
  const [post, setPost] = useState<Post | null>(null);
  const [loading, setLoading] = useState(true);

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<IFormInput>();

  useEffect(() => {
    const fetchPost = async () => {
      const query = groq`
        *[_type=='post' && slug.current == $slug][0]
        {
            ...,
            author->,
            categories[]->
        }
      `;
      const fetchedPost = await client.fetch(query, { slug });
      setPost(fetchedPost);
      setLoading(false);
    };

    fetchPost();
  }, [slug]);

  if (loading) return <div>Loading...</div>;
  if (!post) return <div>Post not found</div>;

  const onSubmit: SubmitHandler<IFormInput> = async (data) => {
    await fetch("/api/createComment", {
      method: "POST",
      body: JSON.stringify(data),
    })
      .then(() => console.log(data))
      .catch((err) => console.log(err));
  };

  // 原JSX内容和之前一致,此处省略
  return <>{/* 原组件的JSX代码 */}</>;
};

export default Post;

注意:这个方案无法保留generateStaticParams,因为该函数只能在Server Component中使用,会失去静态生成的性能优势,因此优先推荐第一种拆分组件的方案。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 16:46:15