perf: significantly reduce HTML we send to the client. Up to 4x smaller (#9321)

The biggest difference comes from calling `RenderServerComponent` as a
function, instead of rendering it by using `<RenderServerComponent`.

This gets rid of wasteful blocks of codes sent to the client that look
like this:

![CleanShot 2024-11-18 at 20 41
20@2x](https://github.com/user-attachments/assets/edb67d72-f4a5-459b-93f4-68dc65aeffb6)


HTML size comparison:

## Admin test suite

| View | Before | After |
|------|---------|--------|
| Dashboard | 331 kB | 83 kB |
| collections/custom-views-one Edit | 285 kB | 76.6 kB |

## Fields test suite

| View | Before | After |
|------|---------|--------|
| collections/lexical Edit | 189 kB | 94.4 kB |
| collections/lexical List | 152 kB | 62.9 kB |

## Community test suite

| View | Before | After |
|------|---------|--------|
| Dashboard | 78.9 kB | 43.1 kB |
This commit is contained in:
Alessio Gravili
2024-11-18 21:30:21 -07:00
committed by GitHub
parent 1425d58b57
commit 5d2b0b30b0
25 changed files with 456 additions and 515 deletions

View File

@@ -75,16 +75,16 @@ export const DocumentTab: React.FC<
{Pill || Pill_Component ? (
<Fragment>
&nbsp;
<RenderServerComponent
Component={Pill}
Fallback={Pill_Component}
importMap={payload.importMap}
serverProps={{
{RenderServerComponent({
Component: Pill,
Fallback: Pill_Component,
importMap: payload.importMap,
serverProps: {
i18n,
payload,
permissions,
}}
/>
},
})}
</Fragment>
) : null}
</span>

View File

@@ -80,23 +80,21 @@ export const DocumentTabs: React.FC<{
const { path, tab } = CustomView
if (tab.Component) {
return (
<RenderServerComponent
clientProps={{
path,
}}
Component={tab.Component}
importMap={payload.importMap}
key={`tab-custom-${index}`}
serverProps={{
collectionConfig,
globalConfig,
i18n,
payload,
permissions,
}}
/>
)
return RenderServerComponent({
clientProps: {
path,
},
Component: tab.Component,
importMap: payload.importMap,
key: `tab-custom-${index}`,
serverProps: {
collectionConfig,
globalConfig,
i18n,
payload,
permissions,
},
})
}
return (

View File

@@ -1,8 +1,8 @@
import type { ServerProps } from 'payload'
import type React from 'react'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { PayloadLogo } from '@payloadcms/ui/shared'
import React from 'react'
export const Logo: React.FC<ServerProps> = (props) => {
const { i18n, locale, params, payload, permissions, searchParams, user } = props
@@ -17,20 +17,18 @@ export const Logo: React.FC<ServerProps> = (props) => {
} = {},
} = payload.config
return (
<RenderServerComponent
Component={CustomLogo}
Fallback={PayloadLogo}
importMap={payload.importMap}
serverProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
/>
)
return RenderServerComponent({
Component: CustomLogo,
Fallback: PayloadLogo,
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
},
})
}

View File

@@ -59,13 +59,28 @@ export const DefaultNav: React.FC<NavProps> = async (props) => {
const navPreferences = await getNavPrefs({ payload, user })
const LogoutComponent = RenderServerComponent({
Component: logout?.Button,
Fallback: Logout,
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
},
})
return (
<NavWrapper baseClass={baseClass}>
<nav className={`${baseClass}__wrap`}>
<RenderServerComponent
Component={beforeNavLinks}
importMap={payload.importMap}
serverProps={{
{RenderServerComponent({
Component: beforeNavLinks,
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
@@ -73,13 +88,13 @@ export const DefaultNav: React.FC<NavProps> = async (props) => {
permissions,
searchParams,
user,
}}
/>
},
})}
<DefaultNavClient groups={groups} navPreferences={navPreferences} />
<RenderServerComponent
Component={afterNavLinks}
importMap={payload.importMap}
serverProps={{
{RenderServerComponent({
Component: afterNavLinks,
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
@@ -87,24 +102,9 @@ export const DefaultNav: React.FC<NavProps> = async (props) => {
permissions,
searchParams,
user,
}}
/>
<div className={`${baseClass}__controls`}>
<RenderServerComponent
Component={logout?.Button}
Fallback={Logout}
importMap={payload.importMap}
serverProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
/>
</div>
},
})}
<div className={`${baseClass}__controls`}>{LogoutComponent}</div>
</nav>
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__header-content`}>

View File

@@ -11,20 +11,18 @@ type Args = {
}
export function NestProviders({ children, importMap, providers }: Args): React.ReactNode {
return (
<RenderServerComponent
clientProps={{
children:
providers.length > 1 ? (
<NestProviders importMap={importMap} providers={providers.slice(1)}>
{children}
</NestProviders>
) : (
children
),
}}
Component={providers[0]}
importMap={importMap}
/>
)
return RenderServerComponent({
clientProps: {
children:
providers.length > 1 ? (
<NestProviders importMap={importMap} providers={providers.slice(1)}>
{children}
</NestProviders>
) : (
children
),
},
Component: providers[0],
importMap,
})
}

View File

@@ -20,6 +20,14 @@ export const OGImage: React.FC<{
leader,
title,
}) => {
const IconComponent = RenderServerComponent({
clientProps: {
fill: 'white',
},
Component: Icon,
Fallback,
importMap,
})
return (
<div
style={{
@@ -95,14 +103,7 @@ export const OGImage: React.FC<{
width: '38px',
}}
>
<RenderServerComponent
clientProps={{
fill: 'white',
}}
Component={Icon}
Fallback={Fallback}
importMap={importMap}
/>
{IconComponent}
</div>
</div>
)

View File

@@ -56,13 +56,15 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
? viewActions.reduce((acc, action) => {
if (action) {
if (typeof action === 'object') {
acc[action.path] = (
<RenderServerComponent Component={action} importMap={payload.importMap} />
)
acc[action.path] = RenderServerComponent({
Component: action,
importMap: payload.importMap,
})
} else {
acc[action] = (
<RenderServerComponent Component={action} importMap={payload.importMap} />
)
acc[action] = RenderServerComponent({
Component: action,
importMap: payload.importMap,
})
}
}
@@ -72,15 +74,32 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
}
}, [viewActions, payload])
const NavComponent = RenderServerComponent({
clientProps: { clientProps: { visibleEntities } },
Component: CustomNav,
Fallback: DefaultNav,
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
visibleEntities,
},
})
return (
<EntityVisibilityProvider visibleEntities={visibleEntities}>
<BulkUploadProvider>
<ActionsProvider Actions={Actions}>
<RenderServerComponent
clientProps={{ clientProps: { visibleEntities } }}
Component={CustomHeader}
importMap={payload.importMap}
serverProps={{
{RenderServerComponent({
clientProps: { clientProps: { visibleEntities } },
Component: CustomHeader,
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
@@ -89,8 +108,8 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
searchParams,
user,
visibleEntities,
}}
/>
},
})}
<div style={{ position: 'relative' }}>
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
<div className={`${baseClass}__nav-toggler-container`} id="nav-toggler">
@@ -100,39 +119,24 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
</div>
</div>
<Wrapper baseClass={baseClass} className={className}>
<RenderServerComponent
clientProps={{ clientProps: { visibleEntities } }}
Component={CustomNav}
Fallback={DefaultNav}
importMap={payload.importMap}
serverProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
visibleEntities,
}}
/>
{NavComponent}
<div className={`${baseClass}__wrap`}>
<AppHeader
CustomAvatar={
avatar !== 'gravatar' && avatar !== 'default' ? (
<RenderServerComponent
Component={avatar.Component}
importMap={payload.importMap}
/>
) : undefined
avatar !== 'gravatar' && avatar !== 'default'
? RenderServerComponent({
Component: avatar.Component,
importMap: payload.importMap,
})
: undefined
}
CustomIcon={
components?.graphics?.Icon ? (
<RenderServerComponent
Component={components.graphics.Icon}
importMap={payload.importMap}
/>
) : undefined
components?.graphics?.Icon
? RenderServerComponent({
Component: components.graphics.Icon,
importMap: payload.importMap,
})
: undefined
}
/>
{children}

View File

@@ -137,11 +137,11 @@ export const Account: React.FC<AdminViewProps> = async ({
permissions={permissions}
/>
<HydrateAuthProvider permissions={permissions} />
<RenderServerComponent
Component={config.admin?.components?.views?.account?.Component}
Fallback={EditView}
importMap={payload.importMap}
serverProps={{
{RenderServerComponent({
Component: config.admin?.components?.views?.account?.Component,
Fallback: EditView,
importMap: payload.importMap,
serverProps: {
i18n,
initPageResult,
locale,
@@ -151,8 +151,8 @@ export const Account: React.FC<AdminViewProps> = async ({
routeSegments: [],
searchParams,
user,
}}
/>
},
})}
<AccountClient />
</EditDepthProvider>
</DocumentInfoProvider>

View File

@@ -49,11 +49,11 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
return (
<div className={baseClass}>
<Gutter className={`${baseClass}__wrap`}>
{beforeDashboard && (
<RenderServerComponent
Component={beforeDashboard}
importMap={payload.importMap}
serverProps={{
{beforeDashboard &&
RenderServerComponent({
Component: beforeDashboard,
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
@@ -61,9 +61,9 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
permissions,
searchParams,
user,
}}
/>
)}
},
})}
<Fragment>
{!navGroups || navGroups?.length === 0 ? (
<p>no nav groups....</p>
@@ -168,11 +168,11 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
})
)}
</Fragment>
{afterDashboard && (
<RenderServerComponent
Component={afterDashboard}
importMap={payload.importMap}
serverProps={{
{afterDashboard &&
RenderServerComponent({
Component: afterDashboard,
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
@@ -180,9 +180,8 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
permissions,
searchParams,
user,
}}
/>
)}
},
})}
</Gutter>
</div>
)

View File

@@ -108,15 +108,15 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
<Fragment>
<HydrateAuthProvider permissions={permissions} />
<SetStepNav nav={[]} />
<RenderServerComponent
clientProps={{
{RenderServerComponent({
clientProps: {
Link,
locale,
}}
Component={config.admin?.components?.views?.dashboard?.Component}
Fallback={DefaultDashboard}
importMap={payload.importMap}
serverProps={{
},
Component: config.admin?.components?.views?.dashboard?.Component,
Fallback: DefaultDashboard,
importMap: payload.importMap,
serverProps: {
globalData,
i18n,
Link,
@@ -128,8 +128,8 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
searchParams,
user,
visibleEntities,
}}
/>
},
})}
</Fragment>
)
}

View File

@@ -345,27 +345,23 @@ export const renderDocument = async ({
)}
<HydrateAuthProvider permissions={permissions} />
<EditDepthProvider>
{ErrorView ? (
<RenderServerComponent
clientProps={clientProps}
Component={ErrorView.ComponentConfig || ErrorView.Component}
importMap={importMap}
serverProps={serverProps}
/>
) : (
<RenderServerComponent
clientProps={clientProps}
Component={
RootViewOverride
{ErrorView
? RenderServerComponent({
clientProps,
Component: ErrorView.ComponentConfig || ErrorView.Component,
importMap,
serverProps,
})
: RenderServerComponent({
clientProps,
Component: RootViewOverride
? RootViewOverride
: CustomView?.ComponentConfig || CustomView?.Component
? CustomView?.ComponentConfig || CustomView?.Component
: DefaultView?.ComponentConfig || DefaultView?.Component
}
importMap={importMap}
serverProps={serverProps}
/>
)}
: DefaultView?.ComponentConfig || DefaultView?.Component,
importMap,
serverProps,
})}
</EditDepthProvider>
</DocumentInfoProvider>
),

View File

@@ -34,9 +34,10 @@ export const renderDocumentSlots: (args: {
globalConfig?.admin?.components?.elements?.PreviewButton
if (isPreviewEnabled && CustomPreviewButton) {
components.PreviewButton = (
<RenderServerComponent Component={CustomPreviewButton} importMap={req.payload.importMap} />
)
components.PreviewButton = RenderServerComponent({
Component: CustomPreviewButton,
importMap: req.payload.importMap,
})
}
const descriptionFromConfig =
@@ -54,14 +55,12 @@ export const renderDocumentSlots: (args: {
const hasDescription = CustomDescription || staticDescription
if (hasDescription) {
components.Description = (
<RenderServerComponent
clientProps={{ description: staticDescription }}
Component={CustomDescription}
Fallback={ViewDescription}
importMap={req.payload.importMap}
/>
)
components.Description = RenderServerComponent({
clientProps: { description: staticDescription },
Component: CustomDescription,
Fallback: ViewDescription,
importMap: req.payload.importMap,
})
}
if (hasSavePermission) {
@@ -71,12 +70,10 @@ export const renderDocumentSlots: (args: {
globalConfig?.admin?.components?.elements?.PublishButton
if (CustomPublishButton) {
components.PublishButton = (
<RenderServerComponent
Component={CustomPublishButton}
importMap={req.payload.importMap}
/>
)
components.PublishButton = RenderServerComponent({
Component: CustomPublishButton,
importMap: req.payload.importMap,
})
}
const CustomSaveDraftButton =
collectionConfig?.admin?.components?.edit?.SaveDraftButton ||
@@ -87,12 +84,10 @@ export const renderDocumentSlots: (args: {
(globalConfig?.versions?.drafts && !globalConfig?.versions?.drafts?.autosave)
if ((draftsEnabled || unsavedDraftWithValidations) && CustomSaveDraftButton) {
components.SaveDraftButton = (
<RenderServerComponent
Component={CustomSaveDraftButton}
importMap={req.payload.importMap}
/>
)
components.SaveDraftButton = RenderServerComponent({
Component: CustomSaveDraftButton,
importMap: req.payload.importMap,
})
}
} else {
const CustomSaveButton =
@@ -100,9 +95,10 @@ export const renderDocumentSlots: (args: {
globalConfig?.admin?.components?.elements?.SaveButton
if (CustomSaveButton) {
components.SaveButton = (
<RenderServerComponent Component={CustomSaveButton} importMap={req.payload.importMap} />
)
components.SaveButton = RenderServerComponent({
Component: CustomSaveButton,
importMap: req.payload.importMap,
})
}
}
}

View File

@@ -243,18 +243,18 @@ export const renderListView = async (
modifySearchParams={!isInDrawer}
preferenceKey={preferenceKey}
>
<RenderServerComponent
clientProps={clientProps}
Component={collectionConfig?.admin?.components?.views?.list?.Component}
Fallback={DefaultListView}
importMap={payload.importMap}
serverProps={{
{RenderServerComponent({
clientProps,
Component: collectionConfig?.admin?.components?.views?.list?.Component,
Fallback: DefaultListView,
importMap: payload.importMap,
serverProps: {
...sharedServerProps,
data,
listPreferences,
listSearchableFields: collectionConfig.admin.listSearchableFields,
}}
/>
},
})}
</ListQueryProvider>
</Fragment>
),

View File

@@ -24,61 +24,51 @@ export const renderListViewSlots = ({
const result: ListViewSlots = {} as ListViewSlots
if (collectionConfig.admin.components?.afterList) {
result.AfterList = (
<RenderServerComponent
clientProps={clientProps}
Component={collectionConfig.admin.components.afterList}
importMap={payload.importMap}
serverProps={serverProps}
/>
)
result.AfterList = RenderServerComponent({
clientProps,
Component: collectionConfig.admin.components.afterList,
importMap: payload.importMap,
serverProps,
})
}
if (collectionConfig.admin.components?.afterListTable) {
result.AfterListTable = (
<RenderServerComponent
clientProps={clientProps}
Component={collectionConfig.admin.components.afterListTable}
importMap={payload.importMap}
serverProps={serverProps}
/>
)
result.AfterListTable = RenderServerComponent({
clientProps,
Component: collectionConfig.admin.components.afterListTable,
importMap: payload.importMap,
serverProps,
})
}
if (collectionConfig.admin.components?.beforeList) {
result.BeforeList = (
<RenderServerComponent
clientProps={clientProps}
Component={collectionConfig.admin.components.beforeList}
importMap={payload.importMap}
serverProps={serverProps}
/>
)
result.BeforeList = RenderServerComponent({
clientProps,
Component: collectionConfig.admin.components.beforeList,
importMap: payload.importMap,
serverProps,
})
}
if (collectionConfig.admin.components?.beforeListTable) {
result.BeforeListTable = (
<RenderServerComponent
clientProps={clientProps}
Component={collectionConfig.admin.components.beforeListTable}
importMap={payload.importMap}
serverProps={serverProps}
/>
)
result.BeforeListTable = RenderServerComponent({
clientProps,
Component: collectionConfig.admin.components.beforeListTable,
importMap: payload.importMap,
serverProps,
})
}
if (collectionConfig.admin.components?.Description) {
result.Description = (
<RenderServerComponent
clientProps={{
description,
...clientProps,
}}
Component={collectionConfig.admin.components.Description}
importMap={payload.importMap}
serverProps={serverProps}
/>
)
result.Description = RenderServerComponent({
clientProps: {
description,
...clientProps,
},
Component: collectionConfig.admin.components.Description,
importMap: payload.importMap,
serverProps,
})
}
return result

View File

@@ -65,10 +65,10 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
user={user}
/>
</div>
<RenderServerComponent
Component={beforeLogin}
importMap={payload.importMap}
serverProps={{
{RenderServerComponent({
Component: beforeLogin,
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
@@ -76,8 +76,9 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
permissions,
searchParams,
user,
}}
/>
},
})}
{!collectionConfig?.auth?.disableLocalStrategy && (
<LoginForm
prefillEmail={prefillEmail}
@@ -86,10 +87,10 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
searchParams={searchParams}
/>
)}
<RenderServerComponent
Component={afterLogin}
importMap={payload.importMap}
serverProps={{
{RenderServerComponent({
Component: afterLogin,
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
@@ -97,8 +98,8 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
permissions,
searchParams,
user,
}}
/>
},
})}
</Fragment>
)
}

View File

@@ -127,24 +127,22 @@ export const RootPage = async ({
importMap,
})
const RenderedView = (
<RenderServerComponent
clientProps={{ clientConfig }}
Component={DefaultView.payloadComponent}
Fallback={DefaultView.Component}
importMap={importMap}
serverProps={{
...serverProps,
clientConfig,
i18n: initPageResult?.req.i18n,
importMap,
initPageResult,
params,
payload: initPageResult?.req.payload,
searchParams,
}}
/>
)
const RenderedView = RenderServerComponent({
clientProps: { clientConfig },
Component: DefaultView.payloadComponent,
Fallback: DefaultView.Component,
importMap,
serverProps: {
...serverProps,
clientConfig,
i18n: initPageResult?.req.i18n,
importMap,
initPageResult,
params,
payload: initPageResult?.req.payload,
searchParams,
},
})
return (
<Fragment>

View File

@@ -175,10 +175,6 @@ export const sanitizeFields = async ({
}
}
if (typeof field.virtual === 'undefined') {
field.virtual = false
}
if (!field.hooks) {
field.hooks = {}
}

View File

@@ -11,7 +11,7 @@ import { renderField } from '@payloadcms/ui/forms/renderField'
import React from 'react'
import type { SanitizedServerEditorConfig } from '../lexical/config/types.js'
import type { LexicalFieldAdminProps } from '../types.js'
import type { LexicalFieldAdminProps, LexicalRichTextFieldProps } from '../types.js'
// eslint-disable-next-line payload/no-imports-from-exports-dir
import { RichTextField } from '../exports/client/index.js'
@@ -57,20 +57,26 @@ export const RscEntryLexicalField: React.FC<
})
}
return (
<RichTextField
admin={args.admin}
clientFeatures={clientFeatures}
featureClientSchemaMap={featureClientSchemaMap}
field={args.clientField as RichTextFieldClient}
forceRender={args.forceRender}
initialLexicalFormState={initialLexicalFormState}
lexicalEditorConfig={args.sanitizedEditorConfig.lexical}
path={path}
permissions={args.permissions}
readOnly={args.readOnly}
renderedBlocks={args.renderedBlocks}
schemaPath={schemaPath}
/>
)
const props: LexicalRichTextFieldProps = {
admin: args.admin,
clientFeatures,
featureClientSchemaMap,
field: args.clientField as RichTextFieldClient,
forceRender: args.forceRender,
initialLexicalFormState,
lexicalEditorConfig: args.sanitizedEditorConfig.lexical,
path,
permissions: args.permissions,
readOnly: args.readOnly,
renderedBlocks: args.renderedBlocks,
schemaPath,
}
for (const key in props) {
if (!props[key]) {
delete props[key]
}
}
return <RichTextField {...props} />
}

View File

@@ -4,7 +4,8 @@ import type {
Field,
FieldPaths,
RichTextFieldClient,
ServerComponentProps } from 'payload'
ServerComponentProps,
} from 'payload'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { createClientFields, deepCopyObjectSimple } from 'payload'
@@ -56,31 +57,31 @@ export const RscEntrySlateField: React.FC<
componentMap.set(
`leaf.button.${leafObject.name}`,
<RenderServerComponent
clientProps={clientProps}
Component={LeafButton}
importMap={payload.importMap}
/>,
RenderServerComponent({
clientProps,
Component: LeafButton,
importMap: payload.importMap,
}),
)
componentMap.set(
`leaf.component.${leafObject.name}`,
<RenderServerComponent
clientProps={clientProps}
Component={LeafComponent}
importMap={payload.importMap}
/>,
RenderServerComponent({
clientProps,
Component: LeafComponent,
importMap: payload.importMap,
}),
)
if (Array.isArray(leafObject.plugins)) {
leafObject.plugins.forEach((Plugin, i) => {
componentMap.set(
`leaf.plugin.${leafObject.name}.${i}`,
<RenderServerComponent
clientProps={clientProps}
Component={Plugin}
importMap={payload.importMap}
/>,
RenderServerComponent({
clientProps,
Component: Plugin,
importMap: payload.importMap,
}),
)
})
}
@@ -102,31 +103,31 @@ export const RscEntrySlateField: React.FC<
if (ElementButton) {
componentMap.set(
`element.button.${element.name}`,
<RenderServerComponent
clientProps={clientProps}
Component={ElementButton}
importMap={payload.importMap}
/>,
RenderServerComponent({
clientProps,
Component: ElementButton,
importMap: payload.importMap,
}),
)
}
componentMap.set(
`element.component.${element.name}`,
<RenderServerComponent
clientProps={clientProps}
Component={ElementComponent}
importMap={payload.importMap}
/>,
RenderServerComponent({
clientProps,
Component: ElementComponent,
importMap: payload.importMap,
}),
)
if (Array.isArray(element.plugins)) {
element.plugins.forEach((Plugin, i) => {
componentMap.set(
`element.plugin.${element.name}.${i}`,
<RenderServerComponent
clientProps={clientProps}
Component={Plugin}
importMap={payload.importMap}
/>,
RenderServerComponent({
clientProps,
Component: Plugin,
importMap: payload.importMap,
}),
)
})
}

View File

@@ -5,10 +5,7 @@ import React from 'react'
import { removeUndefined } from '../../utilities/removeUndefined.js'
/**
* Can be used to render both MappedComponents and React Components.
*/
export const RenderServerComponent: React.FC<{
type RenderServerComponentFn = (args: {
readonly clientProps?: object
readonly Component?:
| PayloadComponent
@@ -17,18 +14,31 @@ export const RenderServerComponent: React.FC<{
| React.ComponentType[]
readonly Fallback?: React.ComponentType
readonly importMap: ImportMap
readonly key?: string
readonly serverProps?: object
}> = ({ clientProps = {}, Component, Fallback, importMap, serverProps }) => {
}) => React.ReactNode
/**
* Can be used to render both MappedComponents and React Components.
*/
export const RenderServerComponent: RenderServerComponentFn = ({
clientProps = {},
Component,
Fallback,
importMap,
key,
serverProps,
}) => {
if (Array.isArray(Component)) {
return Component.map((c, index) => (
<RenderServerComponent
clientProps={clientProps}
Component={c}
importMap={importMap}
key={index}
serverProps={serverProps}
/>
))
return Component.map((c, index) =>
RenderServerComponent({
clientProps,
Component: c,
importMap,
key: index,
serverProps,
}),
)
}
if (typeof Component === 'function') {
@@ -40,7 +50,7 @@ export const RenderServerComponent: React.FC<{
...(isRSC ? serverProps : {}),
})
return <Component {...sanitizedProps} />
return <Component key={key} {...sanitizedProps} />
}
if (typeof Component === 'string' || isPlainObject(Component)) {
@@ -63,16 +73,17 @@ export const RenderServerComponent: React.FC<{
...(typeof Component === 'object' && Component?.clientProps ? Component.clientProps : {}),
})
return <ResolvedComponent {...sanitizedProps} />
return <ResolvedComponent key={key} {...sanitizedProps} />
}
}
return Fallback ? (
<RenderServerComponent
clientProps={clientProps}
Component={Fallback}
importMap={importMap}
serverProps={serverProps}
/>
) : null
return Fallback
? RenderServerComponent({
clientProps,
Component: Fallback,
importMap,
key,
serverProps,
})
: null
}

View File

@@ -6,7 +6,6 @@ import type {
Field,
PaginatedDocs,
Payload,
PayloadComponent,
SanitizedCollectionConfig,
StaticLabel,
} from 'payload'
@@ -151,9 +150,9 @@ export const buildColumnState = (args: Args): Column[] => {
? _field.admin.components.Label
: undefined
const CustomLabel = CustomLabelToRender ? (
<RenderServerComponent Component={CustomLabelToRender} importMap={payload.importMap} />
) : undefined
const CustomLabel = CustomLabelToRender
? RenderServerComponent({ Component: CustomLabelToRender, importMap: payload.importMap })
: undefined
const fieldAffectsDataSubFields =
field &&
@@ -220,41 +219,25 @@ export const buildColumnState = (args: Args): Column[] => {
_field.admin.components = {}
}
/**
* We have to deep copy all the props we send to the client (= CellComponent.clientProps).
* That way, every editor's field / cell props we send to the client have their own object references.
*
* If we send the same object reference to the client twice (e.g. through some configurations where 2 or more fields
* reference the same editor object, like the root editor), the admin panel may hang indefinitely. This has been happening since
* a newer Next.js update that made it break when sending the same object reference to the client twice.
*
* We can use deepCopyObjectSimple as client props should be JSON-serializable.
*/
const CellComponent: PayloadComponent = _field.editor.CellComponent
if (typeof CellComponent === 'object' && CellComponent.clientProps) {
CellComponent.clientProps = deepCopyObjectSimple(CellComponent.clientProps)
}
CustomCell = (
<RenderServerComponent
clientProps={cellClientProps}
Component={CellComponent}
importMap={payload.importMap}
serverProps={serverProps}
/>
)
CustomCell = RenderServerComponent({
clientProps: cellClientProps,
Component: _field.editor.CellComponent,
importMap: payload.importMap,
serverProps,
})
} else {
CustomCell =
_field?.admin && 'components' in _field.admin && _field.admin.components?.Cell ? (
<RenderServerComponent
clientProps={cellClientProps}
Component={
_field?.admin && 'components' in _field.admin && _field.admin.components?.Cell
}
importMap={payload.importMap}
serverProps={serverProps}
/>
) : undefined
_field?.admin && 'components' in _field.admin && _field.admin.components?.Cell
? RenderServerComponent({
clientProps: cellClientProps,
Component:
_field?.admin &&
'components' in _field.admin &&
_field.admin.components?.Cell,
importMap: payload.importMap,
serverProps,
})
: undefined
}
return (

View File

@@ -18,7 +18,7 @@ export const WithServerSideProps: WithServerSidePropsComponent = ({
return <Component {...propsWithServerOnlyProps} />
}
return <WithServerSideProps {...rest} />
return WithServerSideProps(rest)
}
return null

View File

@@ -2,7 +2,6 @@ import type {
ClientComponentProps,
ClientField,
FieldPaths,
PayloadComponent,
SanitizedFieldPermissions,
ServerComponentProps,
} from 'payload'
@@ -109,20 +108,18 @@ export const renderField: RenderFieldMethod = ({
fieldState.customComponents.RowLabels = []
}
fieldState.customComponents.RowLabels[rowIndex] = (
<RenderServerComponent
clientProps={clientProps}
Component={fieldConfig.admin.components.RowLabel}
importMap={req.payload.importMap}
serverProps={{
...serverProps,
rowLabel: `${getTranslation(fieldConfig.labels.singular, req.i18n)} ${String(
rowIndex + 1,
).padStart(2, '0')}`,
rowNumber: rowIndex + 1,
}}
/>
)
fieldState.customComponents.RowLabels[rowIndex] = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.RowLabel,
importMap: req.payload.importMap,
serverProps: {
...serverProps,
rowLabel: `${getTranslation(fieldConfig.labels.singular, req.i18n)} ${String(
rowIndex + 1,
).padStart(2, '0')}`,
rowNumber: rowIndex + 1,
},
})
}
})
@@ -146,30 +143,12 @@ export const renderField: RenderFieldMethod = ({
fieldConfig.admin.components = {}
}
/**
* We have to deep copy all the props we send to the client (= FieldComponent.clientProps).
* That way, every editor's field / cell props we send to the client have their own object references.
*
* If we send the same object reference to the client twice (e.g. through some configurations where 2 or more fields
* reference the same editor object, like the root editor), the admin panel may hang indefinitely. This has been happening since
* a newer Next.js update that made it break when sending the same object reference to the client twice.
*
* We can use deepCopyObjectSimple as client props should be JSON-serializable.
*/
const FieldComponent: PayloadComponent = fieldConfig.editor.FieldComponent
if (typeof FieldComponent === 'object' && FieldComponent.clientProps) {
FieldComponent.clientProps = deepCopyObjectSimple(FieldComponent.clientProps)
}
fieldState.customComponents.Field = (
<RenderServerComponent
clientProps={clientProps}
Component={FieldComponent}
Fallback={undefined}
importMap={req.payload.importMap}
serverProps={serverProps}
/>
)
fieldState.customComponents.Field = RenderServerComponent({
clientProps,
Component: fieldConfig.editor.FieldComponent,
importMap: req.payload.importMap,
serverProps,
})
break
}
@@ -182,15 +161,13 @@ export const renderField: RenderFieldMethod = ({
continue
}
const Component = fieldConfig.admin.components[key]
fieldState.customComponents[key] = (
<RenderServerComponent
clientProps={clientProps}
Component={Component}
importMap={req.payload.importMap}
key={`field.admin.components.${key}`}
serverProps={serverProps}
/>
)
fieldState.customComponents[key] = RenderServerComponent({
clientProps,
Component,
importMap: req.payload.importMap,
key: `field.admin.components.${key}`,
serverProps,
})
}
}
break
@@ -217,75 +194,63 @@ export const renderField: RenderFieldMethod = ({
if (fieldConfig.admin?.components) {
if ('afterInput' in fieldConfig.admin.components) {
fieldState.customComponents.AfterInput = (
<RenderServerComponent
clientProps={clientProps}
Component={fieldConfig.admin.components.afterInput}
importMap={req.payload.importMap}
key="field.admin.components.afterInput"
serverProps={serverProps}
/>
)
fieldState.customComponents.AfterInput = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.afterInput,
importMap: req.payload.importMap,
key: 'field.admin.components.afterInput',
serverProps,
})
}
if ('beforeInput' in fieldConfig.admin.components) {
fieldState.customComponents.BeforeInput = (
<RenderServerComponent
clientProps={clientProps}
Component={fieldConfig.admin.components.beforeInput}
importMap={req.payload.importMap}
key="field.admin.components.beforeInput"
serverProps={serverProps}
/>
)
fieldState.customComponents.BeforeInput = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.beforeInput,
importMap: req.payload.importMap,
key: 'field.admin.components.beforeInput',
serverProps,
})
}
if ('Description' in fieldConfig.admin.components) {
fieldState.customComponents.Description = (
<RenderServerComponent
clientProps={clientProps}
Component={fieldConfig.admin.components.Description}
importMap={req.payload.importMap}
key="field.admin.components.Description"
serverProps={serverProps}
/>
)
fieldState.customComponents.Description = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Description,
importMap: req.payload.importMap,
key: 'field.admin.components.Description',
serverProps,
})
}
if ('Error' in fieldConfig.admin.components) {
fieldState.customComponents.Error = (
<RenderServerComponent
clientProps={clientProps}
Component={fieldConfig.admin.components.Error}
importMap={req.payload.importMap}
key="field.admin.components.Error"
serverProps={serverProps}
/>
)
fieldState.customComponents.Error = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Error,
importMap: req.payload.importMap,
key: 'field.admin.components.Error',
serverProps,
})
}
if ('Label' in fieldConfig.admin.components) {
fieldState.customComponents.Label = (
<RenderServerComponent
clientProps={clientProps}
Component={fieldConfig.admin.components.Label}
importMap={req.payload.importMap}
key="field.admin.components.Label"
serverProps={serverProps}
/>
)
fieldState.customComponents.Label = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Label,
importMap: req.payload.importMap,
key: 'field.admin.components.Label',
serverProps,
})
}
if ('Field' in fieldConfig.admin.components) {
fieldState.customComponents.Field = (
<RenderServerComponent
clientProps={clientProps}
Component={fieldConfig.admin.components.Field}
importMap={req.payload.importMap}
key="field.admin.components.Field"
serverProps={serverProps}
/>
)
fieldState.customComponents.Field = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Field,
importMap: req.payload.importMap,
key: 'field.admin.components.Field',
serverProps,
})
}
}
}

View File

@@ -29,10 +29,10 @@ export const renderFilters = (
if ('name' in field && field.admin?.components?.Filter) {
acc.set(
field.name,
<RenderServerComponent
Component={field.admin.components?.Filter}
importMap={importMap}
/>,
RenderServerComponent({
Component: field.admin.components?.Filter,
importMap,
}),
)
}

View File

@@ -9,10 +9,10 @@ export const AfterDashboardClient: PayloadServerReactComponent<CustomComponent>
return (
<Banner>
<p>Admin Dependency test component:</p>
<RenderServerComponent
Component={payload.config.admin.dependencies?.myTestComponent}
importMap={payload.importMap}
/>
{RenderServerComponent({
Component: payload.config.admin.dependencies?.myTestComponent,
importMap: payload.importMap,
})}
</Banner>
)
}