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
相关产品推荐
相关产品推荐

