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