网站搜索

reactjs - 在 next13 中传递 render props 无法工作


我正在尝试为 Contentful 的实时预览功能在 Next 13 和应用程序目录中构建一个自定义组件。我的想法是创建一个客户端组件,它接受一个 data 属性,通过允许传递泛型类型来实现类型安全。像这样:

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 })
}

然后我可以将其拉入页面组件,传递我从 Contentful 获取的数据,包含类型,然后获取实时更新。但是,当我尝试以这种方式在页面组件中运行它时,我收到这个错误:

Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
  <... data={{...}} children={function}>
                             ^^^^^^^^^^

这就是我的页面组件的样子:

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>
  )
}

我不明白为什么会返回这个错误;有人有更好的方法来修复它吗,我不知道还能尝试什么。似乎是 useContentfulLiveUpdates 钩子的问题,但 updatedData 的类型是一个对象。

答案#1

在当前阶段,Next.js 中的函数是不可序列化的。渲染属性在 RSC -> RSC 或 RCC -> RCC 中可以工作,但似乎 RSC -> RCC 还不行。在这种情况下,我觉得编译器完全可以被优化得聪明到能够处理这种情况,但我认为 Next.js 13 的 app 目录和 RSC 还需要一些时间来成熟。

在我看来,创建一个中间客户端组件应该可行。

"use client"
// ... 

export const MyLivePreview = ({ data }: { data: WhateverDataType }) => <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>

然后,

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'>
        <MyLivePreview data={data} />
      </main>
    </ContentfulProvider>
  )
}

令人恼火的是,根据我使用 Next.js 13 应用目录的经验,需要进行大量的代码拆分。我想象编译器将会变得更智能,以知道哪些是服务器端,哪些是客户端。

请告诉我这是否有帮助(注意,我实际上并没有测试过这些)。否则我很乐意帮助诊断问题。

答案#2

使用 useServer 钩子可能解决你的问题,请检查代码,并且为了进一步参考,我附上了链接

堆栈溢出:错误:函数不能直接传递给客户端组件,除非你通过标记它为“use server”来显式暴露它

下一个文档:https://nextjs.org/docs/getting-started/react-essentials#when-to-use-server-and-client-components

github :https://github.com/vercel/next.js/discussions/47846

// page.tsx

"use server";

import { ClientComponent } from './ClientComponent.tsx';

async function deleteItem(itemId: string) {
  "use server"; // mark function as a server action (fixes the error)

  // TODO add item deletion logic
  return null;
}

export function Page() {
  return <ClientComponent deleteItem={deleteItem} />
}
// ClientComponent.tsx

export function ClientComponent({ deleteItem }) {
  return (
    <button onClick={async () => {
      await deleteItem("foobar");
      alert("item has been deleted");
    }}>
      delete item
    </button>
  );
}