Next.js 13传递Render Props失效,遇函数无法传给客户端组件错误
问题场景
在Next.js 13 App目录中构建Contentful实时预览组件时,使用自定义LivePreviewWrapper客户端组件(带泛型类型安全),将其引入Server Component页面后,传入Contentful数据并使用render prop模式渲染内容,触发以下错误:
Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
<... data={{...}} children={function}>
^^^^^^^^^^
相关代码
LivePreviewWrapper.tsx
'use client' import { useContentfulLiveUpdates } from '@contentful/live-preview/react' const isFunction = <T extends CallableFunction = CallableFunction>(value: unknown): value is T => typeof value === 'function' export const runIfFunction = <T, U>(valueOrFn: T | ((...fnArgs: U[]) => T), ...args: U[]) => { return isFunction(valueOrFn) ? valueOrFn(...args) : valueOrFn } type MaybeRenderProp<P> = React.ReactNode | ((props: P) => React.ReactNode) type LivePreviewWrapperProps<T> = { children: MaybeRenderProp<{ updatedData: T }> data: T } export const LivePreviewWrapper = <T extends Record<string, unknown>>({ children, data }: LivePreviewWrapperProps<T>) => { const updatedData = useContentfulLiveUpdates<T>(data, { locale: 'en-US' }) return runIfFunction(children, { updatedData }) }
页面组件(ContentfulLandingPage.tsx)
import { notFound } from 'next/navigation' import { ContentfulCta, type ContentfulCtaProps } from '~/components/contentful/cta' import { ContentfulGrid, type ContentfulGridProps } from '~/components/contentful/grid' import { ContentfulHero, type ContentfulHeroProps } from '~/components/contentful/hero' import { ContentfulHighlight, type ContentfulHighlightProps } from '~/components/contentful/highlight' import { ContentfulHorizontalTabs, type ContentfulHorizontalTabsProps } from '~/components/contentful/horizontal-tabs' import { ContentfulSpeaker, type ContentfulSpeakerProps } from '~/components/contentful/speaker' import { ContentfulTextSession, type ContentfulTextSessionProps } from '~/components/contentful/text' import { LogoList } from '~/components/marketing/logo-list' import { LivePreviewWrapper } from '~/components/shared/live-preview-wrapper' import { getContentfulParams, getPageBySlug, type ContentfulPage } from '~/models/contentful' import { ContentfulProvider } from '~/providers/contentful' import { type TypePageSkeleton } from '~/types/generated/contentful' import { type PageParamsWithSearch } from '~/types/helpers' import { formatParamsSlug } from '~/utils/core' import '@contentful/live-preview/style.css' type LandingPageParams = PageParamsWithSearch<{ slug: string[] }> export const generateStaticParams = async () => { const pages = await getContentfulParams<TypePageSkeleton>('page') return pages.items.map((page) => ({ params: { slug: formatParamsSlug(page.fields.slug) } })) } export const revalidate = 60 export const dynamic = 'force-dynamic' export default async function ContentfulLandingPage({ params, searchParams }: LandingPageParams) { const isDraftMode = searchParams?.draftMode === 'enabled' const data = await getPageBySlug({ slug: params.slug, isDraftMode }) if (!data) { notFound() } return ( <ContentfulProvider> <main className='pb-8 lg:pb-12'> <LivePreviewWrapper<ContentfulPage> data={data}> {({ updatedData }) => { return ( <div className='space-y-8'> {updatedData.fields.body.map((entry) => { if (!entry) return null switch (entry.sys.contentType.sys.id) { case 'hero': { return <ContentfulHero key={entry.sys.id} entry={entry as ContentfulHeroProps} /> } case 'highlightSession': { return <ContentfulHighlight key={entry.sys.id} entry={entry as ContentfulHighlightProps} /> } case 'grid': { return <ContentfulGrid key={entry.sys.id} entry={entry as ContentfulGridProps} /> } case 'textSession': { return <ContentfulTextSession key={entry.sys.id} entry={entry as ContentfulTextSessionProps} /> } case 'speaker': { return <ContentfulSpeaker key={entry.sys.id} entry={entry as ContentfulSpeakerProps} /> } case 'cta': { return <ContentfulCta key={entry.sys.id} entry={entry as ContentfulCtaProps} /> } case 'horizontalTabs': { return ( <ContentfulHorizontalTabs key={entry.sys.id} entry={entry as ContentfulHorizontalTabsProps} /> ) } case 'customersLogos': { return <LogoList key={entry.sys.id} /> } default: console.warn(entry.sys.contentType, 'was not handled') return null } })} </div> ) }} </LivePreviewWrapper> </main> </ContentfulProvider> ) }
错误原因
Next.js 13 App Router强制要求Server Component传递给Client Component的props必须是可序列化的(如字符串、数字、普通对象等),函数无法被序列化。你的页面组件是Server Component(async函数),而传递给LivePreviewWrapper的children是一个箭头函数(render prop),这违反了Server→Client组件的props传递规则,因此触发错误。
修复方案
方案1:提取渲染逻辑为独立Client Component(推荐)
将依赖updatedData的渲染逻辑移到单独的Client Component中,避免从Server Component传递函数。
步骤1:创建PageContent.tsx(Client Component)
'use client' import { ContentfulCta, type ContentfulCtaProps } from '~/components/contentful/cta' import { ContentfulGrid, type ContentfulGridProps } from '~/components/contentful/grid' import { ContentfulHero, type ContentfulHeroProps } from '~/components/contentful/hero' import { ContentfulHighlight, type ContentfulHighlightProps } from '~/components/contentful/highlight' import { ContentfulHorizontalTabs, type ContentfulHorizontalTabsProps } from '~/components/contentful/horizontal-tabs' import { ContentfulSpeaker, type ContentfulSpeakerProps } from '~/components/contentful/speaker' import { ContentfulTextSession, type ContentfulTextSessionProps } from '~/components/contentful/text' import { LogoList } from '~/components/marketing/logo-list' import type { ContentfulPage } from '~/models/contentful' type PageContentProps = { updatedData: ContentfulPage } export default function PageContent({ updatedData }: PageContentProps) { return ( <div className='space-y-8'> {updatedData.fields.body.map((entry) => { if (!entry) return null switch (entry.sys.contentType.sys.id) { case 'hero': return <ContentfulHero key={entry.sys.id} entry={entry as ContentfulHeroProps} /> case 'highlightSession': return <ContentfulHighlight key={entry.sys.id} entry={entry as ContentfulHighlightProps} /> case 'grid': return <ContentfulGrid key={entry.sys.id} entry={entry as ContentfulGridProps} /> case 'textSession': return <ContentfulTextSession key={entry.sys.id} entry={entry as ContentfulTextSessionProps} /> case 'speaker': return <ContentfulSpeaker key={entry.sys.id} entry={entry as ContentfulSpeakerProps} /> case 'cta': return <ContentfulCta key={entry.sys.id} entry={entry as ContentfulCtaProps} /> case 'horizontalTabs': return <ContentfulHorizontalTabs key={entry.sys.id} entry={entry as ContentfulHorizontalTabsProps} /> case 'customersLogos': return <LogoList key={entry.sys.id} /> default: console.warn(entry.sys.contentType, 'was not handled') return null } })} </div> ) }
步骤2:修改LivePreviewWrapper.tsx
移除render prop模式,改为接收组件并传递updatedData:
'use client' import { useContentfulLiveUpdates } from '@contentful/live-preview/react' import type { ContentfulPage } from '~/models/contentful' type LivePreviewWrapperProps = { data: ContentfulPage component: React.ComponentType<{ updatedData: ContentfulPage }> } export const LivePreviewWrapper = ({ data, component: Component }: LivePreviewWrapperProps) => { const updatedData = useContentfulLiveUpdates<ContentfulPage>(data, { locale: 'en-US' }) return <Component updatedData={updatedData} /> }
步骤3:更新页面组件
使用新的LivePreviewWrapper方式传递组件:
// ...保留原有导入,新增PageContent导入 import PageContent from './page-content' export default async function ContentfulLandingPage({ params, searchParams }: LandingPageParams) { const isDraftMode = searchParams?.draftMode === 'enabled' const data = await getPageBySlug({ slug: params.slug, isDraftMode }) if (!data) { notFound() } return ( <ContentfulProvider> <main className='pb-8 lg:pb-12'> <LivePreviewWrapper data={data} component={PageContent} /> </main> </ContentfulProvider> ) }
方案2:将页面转为Client Component(不推荐)
如果页面不需要Server Component的特性(如服务器端数据获取、静态生成),可以给页面添加'use client'指令,使其成为Client Component,这样就可以传递函数作为props:
'use client' // 新增该行 import { notFound } from 'next/navigation' // ...保留其他导入和代码
注意:此方案会丢失Server Component的性能优势,且无法使用generateStaticParams、revalidate等Server特有的配置,需调整数据获取逻辑到客户端。
内容的提问来源于stack exchange,提问作者Jesse Winton

