From 203ecc1334429a8b77177337c8649ece1abdaeed Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Sun, 3 Nov 2024 04:54:42 +0800 Subject: [PATCH] perf(CollectionField): use custom RecursionField component to avoid unnecessary re-renders --- .../collection-field/CollectionField.tsx | 66 +------- .../src/formily/NocoBaseRecursionField.tsx | 148 ++++++++++++++++++ .../schema-component/antd/table-v2/Table.tsx | 27 +++- 3 files changed, 174 insertions(+), 67 deletions(-) create mode 100644 packages/core/client/src/formily/NocoBaseRecursionField.tsx diff --git a/packages/core/client/src/data-source/collection-field/CollectionField.tsx b/packages/core/client/src/data-source/collection-field/CollectionField.tsx index 12cde9197d..1ddc895a04 100644 --- a/packages/core/client/src/data-source/collection-field/CollectionField.tsx +++ b/packages/core/client/src/data-source/collection-field/CollectionField.tsx @@ -7,14 +7,11 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Field } from '@formily/core'; -import { connect, Schema, useField, useFieldSchema } from '@formily/react'; -import { merge } from '@formily/shared'; -import { concat } from 'lodash'; -import React, { useEffect } from 'react'; +import { connect, useFieldSchema } from '@formily/react'; +import React from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps'; -import { ErrorFallback, useCompile, useComponent } from '../../schema-component'; +import { ErrorFallback, useComponent } from '../../schema-component'; import { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider'; type Props = { @@ -22,64 +19,15 @@ type Props = { children?: React.ReactNode; }; -const setFieldProps = (field: Field, key: string, value: any) => { - if (field[key] === undefined) { - field[key] = value; - } -}; - -const setRequired = (field: Field, fieldSchema: Schema, uiSchema: Schema) => { - if (typeof fieldSchema['required'] === 'undefined') { - field.required = !!uiSchema['required']; - } -}; - -/** - * TODO: 初步适配 - * @internal - */ export const CollectionFieldInternalField: React.FC = (props: Props) => { - const compile = useCompile(); - const field = useField(); const fieldSchema = useFieldSchema(); - const { uiSchema: uiSchemaOrigin, defaultValue } = useCollectionField(); + const { uiSchema } = useCollectionField(); const Component = useComponent( - fieldSchema['x-component-props']?.['component'] || uiSchemaOrigin?.['x-component'] || 'Input', + fieldSchema['x-component-props']?.['component'] || uiSchema?.['x-component'] || 'Input', ); - const dynamicProps = useDynamicComponentProps(uiSchemaOrigin?.['x-use-component-props'], props); + const dynamicProps = useDynamicComponentProps(uiSchema?.['x-use-component-props'], props); - // TODO: 初步适配 - useEffect(() => { - if (!uiSchemaOrigin) { - return; - } - const uiSchema = compile(uiSchemaOrigin); - setFieldProps(field, 'content', uiSchema['x-content']); - setFieldProps(field, 'title', uiSchema.title); - setFieldProps(field, 'description', uiSchema.description); - - if (fieldSchema.default == null && defaultValue != null) { - setFieldProps(field, 'initialValue', defaultValue); - } - - if (!field.validator && (uiSchema['x-validator'] || fieldSchema['x-validator'])) { - const concatSchema = concat([], uiSchema['x-validator'] || [], fieldSchema['x-validator'] || []); - field.validator = concatSchema; - } - if (fieldSchema['x-disabled'] === true) { - field.disabled = true; - } - if (fieldSchema['x-read-pretty'] === true) { - field.readPretty = true; - } - setRequired(field, fieldSchema, uiSchema); - // @ts-ignore - field.dataSource = uiSchema.enum; - const originalProps = uiSchema['x-component-props'] || {}; - field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {}); - }, [uiSchemaOrigin]); - - if (!uiSchemaOrigin) return null; + if (!uiSchema) return null; return ; }; diff --git a/packages/core/client/src/formily/NocoBaseRecursionField.tsx b/packages/core/client/src/formily/NocoBaseRecursionField.tsx new file mode 100644 index 0000000000..bfe3b6d224 --- /dev/null +++ b/packages/core/client/src/formily/NocoBaseRecursionField.tsx @@ -0,0 +1,148 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { GeneralField } from '@formily/core'; +import { + ArrayField, + Field, + IRecursionFieldProps, + ISchema, + ObjectField, + ReactFC, + Schema, + SchemaContext, + useExpressionScope, + useField, + VoidField, +} from '@formily/react'; +import { isBool, isFn, isValid, merge } from '@formily/shared'; +import _ from 'lodash'; +import React, { Fragment, useMemo } from 'react'; + +interface INocoBaseRecursionFieldProps extends IRecursionFieldProps { + /** + * Default Schema for collection fields + */ + uiSchema?: ISchema; +} + +const useFieldProps = (schema: Schema) => { + const scope = useExpressionScope(); + return schema.toFieldProps({ + scope, + }) as any; +}; + +const useBasePath = (props: IRecursionFieldProps) => { + const parent = useField(); + if (props.onlyRenderProperties) { + return props.basePath || parent?.address.concat(props.name); + } + return props.basePath || parent?.address; +}; + +/** + * Based on @formily/react v2.3.2 RecursionField component + * Modified to better adapt to NocoBase's needs + */ +export const NocoBaseRecursionField: ReactFC = React.memo((props) => { + const basePath = useBasePath(props); + const fieldSchema = useMemo(() => new Schema(props.schema), [props.schema]); + + // Merge default Schema of collection fields + const mergedFieldSchema = useMemo(() => { + if (props.uiSchema) { + const clonedSchema = _.cloneDeep(props.schema); + if (props.onlyRenderProperties) { + if (!clonedSchema.properties) { + throw new Error('[NocoBaseRecursionField]: properties is required'); + } + + const firstPropertyKey = Object.keys(clonedSchema.properties)[0]; + const firstPropertyValue = Object.values(clonedSchema.properties)[0]; + clonedSchema.properties[firstPropertyKey] = merge(props.uiSchema, firstPropertyValue); + return new Schema(clonedSchema); + } + return new Schema(merge(props.uiSchema, clonedSchema)); + } + + return fieldSchema; + }, [fieldSchema, props.onlyRenderProperties, props.schema, props.uiSchema]); + + const fieldProps = useFieldProps(mergedFieldSchema); + const renderProperties = (field?: GeneralField) => { + if (props.onlyRenderSelf) return; + const properties = Schema.getOrderProperties(mergedFieldSchema); + if (!properties.length) return; + return ( + + {properties.map(({ schema: item, key: name }, index) => { + const base = field?.address || basePath; + let schema: Schema = item; + if (isFn(props.mapProperties)) { + const mapped = props.mapProperties(item, name); + if (mapped) { + schema = mapped; + } + } + if (isFn(props.filterProperties)) { + if (props.filterProperties(schema, name) === false) { + return null; + } + } + if (isBool(props.propsRecursion) && props.propsRecursion) { + return ( + + ); + } + return ; + })} + + ); + }; + + const render = () => { + if (!isValid(props.name)) return renderProperties(); + if (mergedFieldSchema.type === 'object') { + if (props.onlyRenderProperties) return renderProperties(); + return ( + + {renderProperties} + + ); + } else if (mergedFieldSchema.type === 'array') { + return ; + } else if (mergedFieldSchema.type === 'void') { + if (props.onlyRenderProperties) return renderProperties(); + return ( + + {renderProperties} + + ); + } + return ; + }; + + if (!fieldSchema) return ; + + // The original fieldSchema is still passed down to maintain compatibility with NocoBase usage. + // fieldSchema stores some user-defined content. If we pass down mergedFieldSchema instead, + // some default schema values would also be saved in fieldSchema. + return {render()}; +}); + +NocoBaseRecursionField.displayName = 'NocoBaseRecursionField'; diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx index 3cc0eee120..2ae88a0eb0 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx @@ -13,7 +13,7 @@ import { SortableContext, SortableContextProps, useSortable } from '@dnd-kit/sor import { css, cx } from '@emotion/css'; import { ArrayField } from '@formily/core'; import { spliceArrayState } from '@formily/core/esm/shared/internals'; -import { RecursionField, Schema, observer, useField, useFieldSchema } from '@formily/react'; +import { Schema, observer, useField, useFieldSchema } from '@formily/react'; import { action, raw } from '@formily/reactive'; import { uid } from '@formily/shared'; import { isPortalInBody } from '@nocobase/utils/client'; @@ -38,6 +38,7 @@ import { import { useACLFieldWhitelist } from '../../../acl/ACLProvider'; import { useTableBlockContext } from '../../../block-provider/TableBlockProvider'; import { isNewRecord } from '../../../data-source/collection-record/isNewRecord'; +import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; import { withSkeletonComponent } from '../../../hoc/withSkeletonComponent'; import { useSatisfiedActionValues } from '../../../schema-settings/LinkageRules/useActionValues'; @@ -47,7 +48,6 @@ import { ColumnFieldProvider } from './components/ColumnFieldProvider'; import { TableSkeleton } from './TableSkeleton'; import { extractIndex, isCollectionFieldComponent, isColumnComponent } from './utils'; -const RecursionFieldMemo = React.memo(RecursionField); const InViewContext = React.createContext(false); const useArrayField = (props) => { @@ -95,14 +95,14 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat const { designable } = useDesignable(); const { exists, render } = useSchemaInitializerRender(schema['x-initializer'], schema['x-initializer-props']); const parentRecordData = useCollectionParentRecordData(); - const columnsSchema = schema.reduceProperties((buf, s) => { + const columnsSchemas = schema.reduceProperties((buf, s) => { if (isColumnComponent(s) && schemaInWhitelist(Object.values(s.properties || {}).pop())) { return buf.concat([s]); } return buf; }, []); const { current, pageSize } = paginationProps; - const hasChangedColumns = useColumnsDeepMemoized(columnsSchema); + const hasChangedColumns = useColumnsDeepMemoized(columnsSchemas); const schemaToolbarBigger = useMemo(() => { return css` @@ -111,21 +111,27 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat padding: ${token.paddingContentVerticalLG}px ${token.margin}px; } `; - }, [token.paddingContentVerticalLG, token.marginSM]); + }, [token.paddingContentVerticalLG, token.marginSM, token.margin]); const collection = useCollection(); const columns = useMemo( () => - columnsSchema?.map((columnSchema: Schema) => { + columnsSchemas?.map((columnSchema: Schema) => { const collectionFields = columnSchema.reduceProperties((buf, s) => { if (isCollectionFieldComponent(s)) { return buf.concat([s]); } }, []); const dataIndex = collectionFields?.length > 0 ? collectionFields[0].name : columnSchema.name; + const { uiSchema, defaultValue } = collection?.getField(dataIndex) || {}; + + if (uiSchema) { + uiSchema.default = defaultValue; + } + return { - title: , + title: , dataIndex, key: columnSchema.name, sorter: columnSchema['x-component-props']?.['sorter'], @@ -139,7 +145,12 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat - +