perf(CollectionField): use custom RecursionField component to avoid unnecessary re-renders

This commit is contained in:
Zeke Zhang 2024-11-03 04:54:42 +08:00
parent 459c617309
commit 203ecc1334
3 changed files with 174 additions and 67 deletions

View File

@ -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<Field>();
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 <Component {...props} {...dynamicProps} />;
};

View File

@ -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<INocoBaseRecursionFieldProps> = 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 (
<Fragment>
{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 (
<NocoBaseRecursionField
propsRecursion={true}
filterProperties={props.filterProperties}
mapProperties={props.mapProperties}
schema={schema}
key={`${index}-${name}`}
name={name}
basePath={base}
/>
);
}
return <NocoBaseRecursionField schema={schema} key={`${index}-${name}`} name={name} basePath={base} />;
})}
</Fragment>
);
};
const render = () => {
if (!isValid(props.name)) return renderProperties();
if (mergedFieldSchema.type === 'object') {
if (props.onlyRenderProperties) return renderProperties();
return (
<ObjectField {...fieldProps} name={props.name} basePath={basePath}>
{renderProperties}
</ObjectField>
);
} else if (mergedFieldSchema.type === 'array') {
return <ArrayField {...fieldProps} name={props.name} basePath={basePath} />;
} else if (mergedFieldSchema.type === 'void') {
if (props.onlyRenderProperties) return renderProperties();
return (
<VoidField {...fieldProps} name={props.name} basePath={basePath}>
{renderProperties}
</VoidField>
);
}
return <Field {...fieldProps} name={props.name} basePath={basePath} />;
};
if (!fieldSchema) return <Fragment />;
// 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 <SchemaContext.Provider value={fieldSchema}>{render()}</SchemaContext.Provider>;
});
NocoBaseRecursionField.displayName = 'NocoBaseRecursionField';

View File

@ -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: <RecursionFieldMemo name={columnSchema.name} schema={columnSchema} onlyRenderSelf />,
title: <NocoBaseRecursionField name={columnSchema.name} schema={columnSchema} onlyRenderSelf />,
dataIndex,
key: columnSchema.name,
sorter: columnSchema['x-component-props']?.['sorter'],
@ -139,7 +145,12 @@ const useTableColumns = (props: { showDel?: any; isSubTable?: boolean }, paginat
<RecordProvider isNew={isNewRecord(record)} record={record} parent={parentRecordData}>
<ColumnFieldProvider schema={columnSchema} basePath={basePath}>
<span role="button" className={schemaToolbarBigger}>
<RecursionFieldMemo basePath={basePath} schema={columnSchema} onlyRenderProperties />
<NocoBaseRecursionField
basePath={basePath}
schema={columnSchema}
uiSchema={uiSchema}
onlyRenderProperties
/>
</span>
</ColumnFieldProvider>
</RecordProvider>