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 ? ( {Pill || Pill_Component ? (
<Fragment> <Fragment>
&nbsp; &nbsp;
<RenderServerComponent {RenderServerComponent({
Component={Pill} Component: Pill,
Fallback={Pill_Component} Fallback: Pill_Component,
importMap={payload.importMap} importMap: payload.importMap,
serverProps={{ serverProps: {
i18n, i18n,
payload, payload,
permissions, permissions,
}} },
/> })}
</Fragment> </Fragment>
) : null} ) : null}
</span> </span>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ import type {
Field, Field,
PaginatedDocs, PaginatedDocs,
Payload, Payload,
PayloadComponent,
SanitizedCollectionConfig, SanitizedCollectionConfig,
StaticLabel, StaticLabel,
} from 'payload' } from 'payload'
@@ -151,9 +150,9 @@ export const buildColumnState = (args: Args): Column[] => {
? _field.admin.components.Label ? _field.admin.components.Label
: undefined : undefined
const CustomLabel = CustomLabelToRender ? ( const CustomLabel = CustomLabelToRender
<RenderServerComponent Component={CustomLabelToRender} importMap={payload.importMap} /> ? RenderServerComponent({ Component: CustomLabelToRender, importMap: payload.importMap })
) : undefined : undefined
const fieldAffectsDataSubFields = const fieldAffectsDataSubFields =
field && field &&
@@ -220,41 +219,25 @@ export const buildColumnState = (args: Args): Column[] => {
_field.admin.components = {} _field.admin.components = {}
} }
/** CustomCell = RenderServerComponent({
* We have to deep copy all the props we send to the client (= CellComponent.clientProps). clientProps: cellClientProps,
* That way, every editor's field / cell props we send to the client have their own object references. Component: _field.editor.CellComponent,
* importMap: payload.importMap,
* If we send the same object reference to the client twice (e.g. through some configurations where 2 or more fields serverProps,
* 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}
/>
)
} else { } else {
CustomCell = 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 _field?.admin && 'components' in _field.admin && _field.admin.components?.Cell
} ? RenderServerComponent({
importMap={payload.importMap} clientProps: cellClientProps,
serverProps={serverProps} Component:
/> _field?.admin &&
) : undefined 'components' in _field.admin &&
_field.admin.components?.Cell,
importMap: payload.importMap,
serverProps,
})
: undefined
} }
return ( return (

View File

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

View File

@@ -2,7 +2,6 @@ import type {
ClientComponentProps, ClientComponentProps,
ClientField, ClientField,
FieldPaths, FieldPaths,
PayloadComponent,
SanitizedFieldPermissions, SanitizedFieldPermissions,
ServerComponentProps, ServerComponentProps,
} from 'payload' } from 'payload'
@@ -109,20 +108,18 @@ export const renderField: RenderFieldMethod = ({
fieldState.customComponents.RowLabels = [] fieldState.customComponents.RowLabels = []
} }
fieldState.customComponents.RowLabels[rowIndex] = ( fieldState.customComponents.RowLabels[rowIndex] = RenderServerComponent({
<RenderServerComponent clientProps,
clientProps={clientProps} Component: fieldConfig.admin.components.RowLabel,
Component={fieldConfig.admin.components.RowLabel} importMap: req.payload.importMap,
importMap={req.payload.importMap} serverProps: {
serverProps={{
...serverProps, ...serverProps,
rowLabel: `${getTranslation(fieldConfig.labels.singular, req.i18n)} ${String( rowLabel: `${getTranslation(fieldConfig.labels.singular, req.i18n)} ${String(
rowIndex + 1, rowIndex + 1,
).padStart(2, '0')}`, ).padStart(2, '0')}`,
rowNumber: rowIndex + 1, rowNumber: rowIndex + 1,
}} },
/> })
)
} }
}) })
@@ -146,30 +143,12 @@ export const renderField: RenderFieldMethod = ({
fieldConfig.admin.components = {} fieldConfig.admin.components = {}
} }
/** fieldState.customComponents.Field = RenderServerComponent({
* We have to deep copy all the props we send to the client (= FieldComponent.clientProps). clientProps,
* That way, every editor's field / cell props we send to the client have their own object references. Component: fieldConfig.editor.FieldComponent,
* importMap: req.payload.importMap,
* If we send the same object reference to the client twice (e.g. through some configurations where 2 or more fields serverProps,
* 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}
/>
)
break break
} }
@@ -182,15 +161,13 @@ export const renderField: RenderFieldMethod = ({
continue continue
} }
const Component = fieldConfig.admin.components[key] const Component = fieldConfig.admin.components[key]
fieldState.customComponents[key] = ( fieldState.customComponents[key] = RenderServerComponent({
<RenderServerComponent clientProps,
clientProps={clientProps} Component,
Component={Component} importMap: req.payload.importMap,
importMap={req.payload.importMap} key: `field.admin.components.${key}`,
key={`field.admin.components.${key}`} serverProps,
serverProps={serverProps} })
/>
)
} }
} }
break break
@@ -217,75 +194,63 @@ export const renderField: RenderFieldMethod = ({
if (fieldConfig.admin?.components) { if (fieldConfig.admin?.components) {
if ('afterInput' in fieldConfig.admin.components) { if ('afterInput' in fieldConfig.admin.components) {
fieldState.customComponents.AfterInput = ( fieldState.customComponents.AfterInput = RenderServerComponent({
<RenderServerComponent clientProps,
clientProps={clientProps} Component: fieldConfig.admin.components.afterInput,
Component={fieldConfig.admin.components.afterInput} importMap: req.payload.importMap,
importMap={req.payload.importMap} key: 'field.admin.components.afterInput',
key="field.admin.components.afterInput" serverProps,
serverProps={serverProps} })
/>
)
} }
if ('beforeInput' in fieldConfig.admin.components) { if ('beforeInput' in fieldConfig.admin.components) {
fieldState.customComponents.BeforeInput = ( fieldState.customComponents.BeforeInput = RenderServerComponent({
<RenderServerComponent clientProps,
clientProps={clientProps} Component: fieldConfig.admin.components.beforeInput,
Component={fieldConfig.admin.components.beforeInput} importMap: req.payload.importMap,
importMap={req.payload.importMap} key: 'field.admin.components.beforeInput',
key="field.admin.components.beforeInput" serverProps,
serverProps={serverProps} })
/>
)
} }
if ('Description' in fieldConfig.admin.components) { if ('Description' in fieldConfig.admin.components) {
fieldState.customComponents.Description = ( fieldState.customComponents.Description = RenderServerComponent({
<RenderServerComponent clientProps,
clientProps={clientProps} Component: fieldConfig.admin.components.Description,
Component={fieldConfig.admin.components.Description} importMap: req.payload.importMap,
importMap={req.payload.importMap} key: 'field.admin.components.Description',
key="field.admin.components.Description" serverProps,
serverProps={serverProps} })
/>
)
} }
if ('Error' in fieldConfig.admin.components) { if ('Error' in fieldConfig.admin.components) {
fieldState.customComponents.Error = ( fieldState.customComponents.Error = RenderServerComponent({
<RenderServerComponent clientProps,
clientProps={clientProps} Component: fieldConfig.admin.components.Error,
Component={fieldConfig.admin.components.Error} importMap: req.payload.importMap,
importMap={req.payload.importMap} key: 'field.admin.components.Error',
key="field.admin.components.Error" serverProps,
serverProps={serverProps} })
/>
)
} }
if ('Label' in fieldConfig.admin.components) { if ('Label' in fieldConfig.admin.components) {
fieldState.customComponents.Label = ( fieldState.customComponents.Label = RenderServerComponent({
<RenderServerComponent clientProps,
clientProps={clientProps} Component: fieldConfig.admin.components.Label,
Component={fieldConfig.admin.components.Label} importMap: req.payload.importMap,
importMap={req.payload.importMap} key: 'field.admin.components.Label',
key="field.admin.components.Label" serverProps,
serverProps={serverProps} })
/>
)
} }
if ('Field' in fieldConfig.admin.components) { if ('Field' in fieldConfig.admin.components) {
fieldState.customComponents.Field = ( fieldState.customComponents.Field = RenderServerComponent({
<RenderServerComponent clientProps,
clientProps={clientProps} Component: fieldConfig.admin.components.Field,
Component={fieldConfig.admin.components.Field} importMap: req.payload.importMap,
importMap={req.payload.importMap} key: 'field.admin.components.Field',
key="field.admin.components.Field" serverProps,
serverProps={serverProps} })
/>
)
} }
} }
} }

View File

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

View File

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