perf: removes undefined props from rsc requests (#9195)
This is in effort to reduce overall HTML bloat, undefined props still go through the request as `$undefined` and must be explicitly omitted.
This commit is contained in:
@@ -20,7 +20,7 @@ export type ServerOnlyCollectionProperties = keyof Pick<
|
||||
|
||||
export type ServerOnlyCollectionAdminProperties = keyof Pick<
|
||||
SanitizedCollectionConfig['admin'],
|
||||
'baseListFilter' | 'hidden'
|
||||
'baseListFilter' | 'components' | 'hidden'
|
||||
>
|
||||
|
||||
export type ServerOnlyUploadProperties = keyof Pick<
|
||||
@@ -34,7 +34,6 @@ export type ServerOnlyUploadProperties = keyof Pick<
|
||||
|
||||
export type ClientCollectionConfig = {
|
||||
admin: {
|
||||
components: null
|
||||
description?: StaticDescription
|
||||
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
||||
preview?: boolean
|
||||
@@ -76,6 +75,7 @@ const serverOnlyUploadProperties: Partial<ServerOnlyUploadProperties>[] = [
|
||||
const serverOnlyCollectionAdminProperties: Partial<ServerOnlyCollectionAdminProperties>[] = [
|
||||
'hidden',
|
||||
'baseListFilter',
|
||||
'components',
|
||||
// 'preview' is handled separately
|
||||
// `livePreview` is handled separately
|
||||
]
|
||||
@@ -89,7 +89,10 @@ export const createClientCollectionConfig = ({
|
||||
defaultIDType: Payload['config']['db']['defaultIDType']
|
||||
i18n: I18nClient
|
||||
}): ClientCollectionConfig => {
|
||||
const clientCollection = deepCopyObjectSimple(collection) as unknown as ClientCollectionConfig
|
||||
const clientCollection = deepCopyObjectSimple(
|
||||
collection,
|
||||
true,
|
||||
) as unknown as ClientCollectionConfig
|
||||
|
||||
clientCollection.fields = createClientFields({
|
||||
clientFields: clientCollection?.fields || [],
|
||||
@@ -150,8 +153,6 @@ export const createClientCollectionConfig = ({
|
||||
clientCollection.admin.preview = true
|
||||
}
|
||||
|
||||
clientCollection.admin.components = null
|
||||
|
||||
let description = undefined
|
||||
|
||||
if (collection.admin?.description) {
|
||||
@@ -165,7 +166,9 @@ export const createClientCollectionConfig = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (description) {
|
||||
clientCollection.admin.description = description
|
||||
}
|
||||
|
||||
if (
|
||||
'livePreview' in clientCollection.admin &&
|
||||
|
||||
@@ -79,7 +79,7 @@ export const createClientConfig = ({
|
||||
i18n: I18nClient
|
||||
}): ClientConfig => {
|
||||
// We can use deepCopySimple here, as the clientConfig should be JSON serializable anyways, since it will be sent from server => client
|
||||
const clientConfig = deepCopyObjectSimple(config) as unknown as ClientConfig
|
||||
const clientConfig = deepCopyObjectSimple(config, true) as unknown as ClientConfig
|
||||
|
||||
for (const key of serverOnlyConfigProperties) {
|
||||
if (key in clientConfig) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { Payload } from '../../types/index.js'
|
||||
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
|
||||
import { fieldAffectsData } from '../../fields/config/types.js'
|
||||
import { flattenTopLevelFields } from '../../index.js'
|
||||
import { removeUndefined } from '../../utilities/removeUndefined.js'
|
||||
|
||||
// Should not be used - ClientField should be used instead. This is why we don't export ClientField, we don't want people
|
||||
// to accidentally use it instead of ClientField and get confused
|
||||
@@ -99,8 +100,9 @@ export const createClientField = ({
|
||||
clientField.admin.style = {
|
||||
...clientField.admin.style,
|
||||
'--field-width': clientField.admin.width,
|
||||
width: undefined, // avoid needlessly adding this to the element's style attribute
|
||||
}
|
||||
|
||||
delete clientField.admin.style.width // avoid needlessly adding this to the element's style attribute
|
||||
} else {
|
||||
if (!(clientField.admin.style instanceof Object)) {
|
||||
clientField.admin.style = {}
|
||||
@@ -137,19 +139,24 @@ export const createClientField = ({
|
||||
if (incomingField.blocks?.length) {
|
||||
for (let i = 0; i < incomingField.blocks.length; i++) {
|
||||
const block = incomingField.blocks[i]
|
||||
const clientBlock: ClientBlock = {
|
||||
|
||||
// prevent $undefined from being passed through the rsc requests
|
||||
const clientBlock = removeUndefined<ClientBlock>({
|
||||
slug: block.slug,
|
||||
admin: {
|
||||
components: {},
|
||||
custom: block.admin?.custom,
|
||||
},
|
||||
fields: field.blocks?.[i]?.fields || [],
|
||||
imageAltText: block.imageAltText,
|
||||
imageURL: block.imageURL,
|
||||
}) satisfies ClientBlock
|
||||
|
||||
if (block.admin?.custom) {
|
||||
clientBlock.admin = {
|
||||
custom: block.admin.custom,
|
||||
}
|
||||
}
|
||||
|
||||
if (block.labels) {
|
||||
clientBlock.labels = {} as unknown as LabelsClient
|
||||
|
||||
if (block.labels.singular) {
|
||||
if (typeof block.labels.singular === 'function') {
|
||||
clientBlock.labels.singular = block.labels.singular({ t: i18n.t })
|
||||
@@ -184,6 +191,7 @@ export const createClientField = ({
|
||||
|
||||
case 'radio':
|
||||
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 'select': {
|
||||
const field = clientField as RadioFieldClient | SelectFieldClient
|
||||
|
||||
@@ -206,7 +214,6 @@ export const createClientField = ({
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!incomingField?.editor) {
|
||||
throw new MissingEditorProp(incomingField) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
@@ -218,6 +225,7 @@ export const createClientField = ({
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
const field = clientField as unknown as TabsFieldClient
|
||||
|
||||
@@ -325,7 +333,6 @@ export const createClientFields = ({
|
||||
},
|
||||
hidden: true,
|
||||
label: 'ID',
|
||||
localized: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export const createClientGlobalConfig = ({
|
||||
global: SanitizedConfig['globals'][0]
|
||||
i18n: I18nClient
|
||||
}): ClientGlobalConfig => {
|
||||
const clientGlobal = deepCopyObjectSimple(global) as unknown as ClientGlobalConfig
|
||||
const clientGlobal = deepCopyObjectSimple(global, true) as unknown as ClientGlobalConfig
|
||||
|
||||
clientGlobal.fields = createClientFields({
|
||||
clientFields: clientGlobal?.fields || [],
|
||||
|
||||
@@ -98,12 +98,12 @@ Benchmark: https://github.com/AlessioGr/fastest-deep-clone-json/blob/main/test/b
|
||||
* `Set`, `Buffer`, ... are not allowed.
|
||||
* @returns The cloned JSON value.
|
||||
*/
|
||||
export function deepCopyObjectSimple<T extends JsonValue>(value: T): T {
|
||||
export function deepCopyObjectSimple<T extends JsonValue>(value: T, filterUndefined = false): T {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return value
|
||||
} else if (Array.isArray(value)) {
|
||||
return value.map((e) =>
|
||||
typeof e !== 'object' || e === null ? e : deepCopyObjectSimple(e),
|
||||
typeof e !== 'object' || e === null ? e : deepCopyObjectSimple(e, filterUndefined),
|
||||
) as T
|
||||
} else {
|
||||
if (value instanceof Date) {
|
||||
@@ -112,7 +112,13 @@ export function deepCopyObjectSimple<T extends JsonValue>(value: T): T {
|
||||
const ret: { [key: string]: T } = {}
|
||||
for (const k in value) {
|
||||
const v = value[k]
|
||||
ret[k] = typeof v !== 'object' || v === null ? v : (deepCopyObjectSimple(v as T) as any)
|
||||
if (filterUndefined && v === undefined) {
|
||||
continue
|
||||
}
|
||||
ret[k] =
|
||||
typeof v !== 'object' || v === null
|
||||
? v
|
||||
: (deepCopyObjectSimple(v as T, filterUndefined) as any)
|
||||
}
|
||||
return ret as unknown as T
|
||||
}
|
||||
|
||||
3
packages/payload/src/utilities/removeUndefined.ts
Normal file
3
packages/payload/src/utilities/removeUndefined.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function removeUndefined<T extends object>(obj: T): T {
|
||||
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined)) as T
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
} from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { removeUndefined } from '../../utilities/removeUndefined.js'
|
||||
|
||||
export const getFromImportMap = <TOutput,>(args: {
|
||||
importMap: ImportMap
|
||||
PayloadComponent: PayloadComponent
|
||||
@@ -66,7 +68,13 @@ export const RenderServerComponent: React.FC<{
|
||||
if (typeof Component === 'function') {
|
||||
const isRSC = isReactServerComponentOrFunction(Component)
|
||||
|
||||
return <Component {...clientProps} {...(isRSC ? serverProps : {})} />
|
||||
// prevent $undefined from being passed through the rsc requests
|
||||
const sanitizedProps = removeUndefined({
|
||||
...clientProps,
|
||||
...(isRSC ? serverProps : {}),
|
||||
})
|
||||
|
||||
return <Component {...sanitizedProps} />
|
||||
}
|
||||
|
||||
if (typeof Component === 'string' || isPlainObject(Component)) {
|
||||
@@ -79,18 +87,17 @@ export const RenderServerComponent: React.FC<{
|
||||
if (ResolvedComponent) {
|
||||
const isRSC = isReactServerComponentOrFunction(ResolvedComponent)
|
||||
|
||||
return (
|
||||
<ResolvedComponent
|
||||
{...clientProps}
|
||||
{...(isRSC ? serverProps : {})}
|
||||
{...(isRSC && typeof Component === 'object' && Component?.serverProps
|
||||
// prevent $undefined from being passed through rsc requests
|
||||
const sanitizedProps = removeUndefined({
|
||||
...clientProps,
|
||||
...(isRSC ? serverProps : {}),
|
||||
...(isRSC && typeof Component === 'object' && Component?.serverProps
|
||||
? Component.serverProps
|
||||
: {})}
|
||||
{...(typeof Component === 'object' && Component?.clientProps
|
||||
? Component.clientProps
|
||||
: {})}
|
||||
/>
|
||||
)
|
||||
: {}),
|
||||
...(typeof Component === 'object' && Component?.clientProps ? Component.clientProps : {}),
|
||||
})
|
||||
|
||||
return <ResolvedComponent {...sanitizedProps} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
packages/ui/src/utilities/removeUndefined.ts
Normal file
3
packages/ui/src/utilities/removeUndefined.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function removeUndefined<T extends object>(obj: T): T {
|
||||
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined)) as T
|
||||
}
|
||||
Reference in New Issue
Block a user