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<
|
export type ServerOnlyCollectionAdminProperties = keyof Pick<
|
||||||
SanitizedCollectionConfig['admin'],
|
SanitizedCollectionConfig['admin'],
|
||||||
'baseListFilter' | 'hidden'
|
'baseListFilter' | 'components' | 'hidden'
|
||||||
>
|
>
|
||||||
|
|
||||||
export type ServerOnlyUploadProperties = keyof Pick<
|
export type ServerOnlyUploadProperties = keyof Pick<
|
||||||
@@ -34,7 +34,6 @@ export type ServerOnlyUploadProperties = keyof Pick<
|
|||||||
|
|
||||||
export type ClientCollectionConfig = {
|
export type ClientCollectionConfig = {
|
||||||
admin: {
|
admin: {
|
||||||
components: null
|
|
||||||
description?: StaticDescription
|
description?: StaticDescription
|
||||||
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
||||||
preview?: boolean
|
preview?: boolean
|
||||||
@@ -76,6 +75,7 @@ const serverOnlyUploadProperties: Partial<ServerOnlyUploadProperties>[] = [
|
|||||||
const serverOnlyCollectionAdminProperties: Partial<ServerOnlyCollectionAdminProperties>[] = [
|
const serverOnlyCollectionAdminProperties: Partial<ServerOnlyCollectionAdminProperties>[] = [
|
||||||
'hidden',
|
'hidden',
|
||||||
'baseListFilter',
|
'baseListFilter',
|
||||||
|
'components',
|
||||||
// 'preview' is handled separately
|
// 'preview' is handled separately
|
||||||
// `livePreview` is handled separately
|
// `livePreview` is handled separately
|
||||||
]
|
]
|
||||||
@@ -89,7 +89,10 @@ export const createClientCollectionConfig = ({
|
|||||||
defaultIDType: Payload['config']['db']['defaultIDType']
|
defaultIDType: Payload['config']['db']['defaultIDType']
|
||||||
i18n: I18nClient
|
i18n: I18nClient
|
||||||
}): ClientCollectionConfig => {
|
}): ClientCollectionConfig => {
|
||||||
const clientCollection = deepCopyObjectSimple(collection) as unknown as ClientCollectionConfig
|
const clientCollection = deepCopyObjectSimple(
|
||||||
|
collection,
|
||||||
|
true,
|
||||||
|
) as unknown as ClientCollectionConfig
|
||||||
|
|
||||||
clientCollection.fields = createClientFields({
|
clientCollection.fields = createClientFields({
|
||||||
clientFields: clientCollection?.fields || [],
|
clientFields: clientCollection?.fields || [],
|
||||||
@@ -150,8 +153,6 @@ export const createClientCollectionConfig = ({
|
|||||||
clientCollection.admin.preview = true
|
clientCollection.admin.preview = true
|
||||||
}
|
}
|
||||||
|
|
||||||
clientCollection.admin.components = null
|
|
||||||
|
|
||||||
let description = undefined
|
let description = undefined
|
||||||
|
|
||||||
if (collection.admin?.description) {
|
if (collection.admin?.description) {
|
||||||
@@ -165,7 +166,9 @@ export const createClientCollectionConfig = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clientCollection.admin.description = description
|
if (description) {
|
||||||
|
clientCollection.admin.description = description
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
'livePreview' in clientCollection.admin &&
|
'livePreview' in clientCollection.admin &&
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const createClientConfig = ({
|
|||||||
i18n: I18nClient
|
i18n: I18nClient
|
||||||
}): ClientConfig => {
|
}): ClientConfig => {
|
||||||
// We can use deepCopySimple here, as the clientConfig should be JSON serializable anyways, since it will be sent from server => client
|
// 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) {
|
for (const key of serverOnlyConfigProperties) {
|
||||||
if (key in clientConfig) {
|
if (key in clientConfig) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type { Payload } from '../../types/index.js'
|
|||||||
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
|
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
|
||||||
import { fieldAffectsData } from '../../fields/config/types.js'
|
import { fieldAffectsData } from '../../fields/config/types.js'
|
||||||
import { flattenTopLevelFields } from '../../index.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
|
// 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
|
// to accidentally use it instead of ClientField and get confused
|
||||||
@@ -99,8 +100,9 @@ export const createClientField = ({
|
|||||||
clientField.admin.style = {
|
clientField.admin.style = {
|
||||||
...clientField.admin.style,
|
...clientField.admin.style,
|
||||||
'--field-width': clientField.admin.width,
|
'--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 {
|
} else {
|
||||||
if (!(clientField.admin.style instanceof Object)) {
|
if (!(clientField.admin.style instanceof Object)) {
|
||||||
clientField.admin.style = {}
|
clientField.admin.style = {}
|
||||||
@@ -137,19 +139,24 @@ export const createClientField = ({
|
|||||||
if (incomingField.blocks?.length) {
|
if (incomingField.blocks?.length) {
|
||||||
for (let i = 0; i < incomingField.blocks.length; i++) {
|
for (let i = 0; i < incomingField.blocks.length; i++) {
|
||||||
const block = incomingField.blocks[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,
|
slug: block.slug,
|
||||||
admin: {
|
|
||||||
components: {},
|
|
||||||
custom: block.admin?.custom,
|
|
||||||
},
|
|
||||||
fields: field.blocks?.[i]?.fields || [],
|
fields: field.blocks?.[i]?.fields || [],
|
||||||
imageAltText: block.imageAltText,
|
imageAltText: block.imageAltText,
|
||||||
imageURL: block.imageURL,
|
imageURL: block.imageURL,
|
||||||
|
}) satisfies ClientBlock
|
||||||
|
|
||||||
|
if (block.admin?.custom) {
|
||||||
|
clientBlock.admin = {
|
||||||
|
custom: block.admin.custom,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block.labels) {
|
if (block.labels) {
|
||||||
clientBlock.labels = {} as unknown as LabelsClient
|
clientBlock.labels = {} as unknown as LabelsClient
|
||||||
|
|
||||||
if (block.labels.singular) {
|
if (block.labels.singular) {
|
||||||
if (typeof block.labels.singular === 'function') {
|
if (typeof block.labels.singular === 'function') {
|
||||||
clientBlock.labels.singular = block.labels.singular({ t: i18n.t })
|
clientBlock.labels.singular = block.labels.singular({ t: i18n.t })
|
||||||
@@ -184,6 +191,7 @@ export const createClientField = ({
|
|||||||
|
|
||||||
case 'radio':
|
case 'radio':
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-fallthrough
|
||||||
case 'select': {
|
case 'select': {
|
||||||
const field = clientField as RadioFieldClient | SelectFieldClient
|
const field = clientField as RadioFieldClient | SelectFieldClient
|
||||||
|
|
||||||
@@ -206,7 +214,6 @@ export const createClientField = ({
|
|||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'richText': {
|
case 'richText': {
|
||||||
if (!incomingField?.editor) {
|
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
|
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
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'tabs': {
|
case 'tabs': {
|
||||||
const field = clientField as unknown as TabsFieldClient
|
const field = clientField as unknown as TabsFieldClient
|
||||||
|
|
||||||
@@ -325,7 +333,6 @@ export const createClientFields = ({
|
|||||||
},
|
},
|
||||||
hidden: true,
|
hidden: true,
|
||||||
label: 'ID',
|
label: 'ID',
|
||||||
localized: undefined,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const createClientGlobalConfig = ({
|
|||||||
global: SanitizedConfig['globals'][0]
|
global: SanitizedConfig['globals'][0]
|
||||||
i18n: I18nClient
|
i18n: I18nClient
|
||||||
}): ClientGlobalConfig => {
|
}): ClientGlobalConfig => {
|
||||||
const clientGlobal = deepCopyObjectSimple(global) as unknown as ClientGlobalConfig
|
const clientGlobal = deepCopyObjectSimple(global, true) as unknown as ClientGlobalConfig
|
||||||
|
|
||||||
clientGlobal.fields = createClientFields({
|
clientGlobal.fields = createClientFields({
|
||||||
clientFields: clientGlobal?.fields || [],
|
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.
|
* `Set`, `Buffer`, ... are not allowed.
|
||||||
* @returns The cloned JSON value.
|
* @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) {
|
if (typeof value !== 'object' || value === null) {
|
||||||
return value
|
return value
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
return value.map((e) =>
|
return value.map((e) =>
|
||||||
typeof e !== 'object' || e === null ? e : deepCopyObjectSimple(e),
|
typeof e !== 'object' || e === null ? e : deepCopyObjectSimple(e, filterUndefined),
|
||||||
) as T
|
) as T
|
||||||
} else {
|
} else {
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
@@ -112,7 +112,13 @@ export function deepCopyObjectSimple<T extends JsonValue>(value: T): T {
|
|||||||
const ret: { [key: string]: T } = {}
|
const ret: { [key: string]: T } = {}
|
||||||
for (const k in value) {
|
for (const k in value) {
|
||||||
const v = value[k]
|
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
|
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'
|
} from 'payload/shared'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import { removeUndefined } from '../../utilities/removeUndefined.js'
|
||||||
|
|
||||||
export const getFromImportMap = <TOutput,>(args: {
|
export const getFromImportMap = <TOutput,>(args: {
|
||||||
importMap: ImportMap
|
importMap: ImportMap
|
||||||
PayloadComponent: PayloadComponent
|
PayloadComponent: PayloadComponent
|
||||||
@@ -66,7 +68,13 @@ export const RenderServerComponent: React.FC<{
|
|||||||
if (typeof Component === 'function') {
|
if (typeof Component === 'function') {
|
||||||
const isRSC = isReactServerComponentOrFunction(Component)
|
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)) {
|
if (typeof Component === 'string' || isPlainObject(Component)) {
|
||||||
@@ -79,18 +87,17 @@ export const RenderServerComponent: React.FC<{
|
|||||||
if (ResolvedComponent) {
|
if (ResolvedComponent) {
|
||||||
const isRSC = isReactServerComponentOrFunction(ResolvedComponent)
|
const isRSC = isReactServerComponentOrFunction(ResolvedComponent)
|
||||||
|
|
||||||
return (
|
// prevent $undefined from being passed through rsc requests
|
||||||
<ResolvedComponent
|
const sanitizedProps = removeUndefined({
|
||||||
{...clientProps}
|
...clientProps,
|
||||||
{...(isRSC ? serverProps : {})}
|
...(isRSC ? serverProps : {}),
|
||||||
{...(isRSC && typeof Component === 'object' && Component?.serverProps
|
...(isRSC && typeof Component === 'object' && Component?.serverProps
|
||||||
? Component.serverProps
|
? Component.serverProps
|
||||||
: {})}
|
: {}),
|
||||||
{...(typeof Component === 'object' && Component?.clientProps
|
...(typeof Component === 'object' && Component?.clientProps ? 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