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

Next.js 13传递Render Props失效,遇函数无法传给客户端组件错误

解决Next.js 13 App目录下Contentful实时预览组件的函数传递错误

问题场景

在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 08:24:56