mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 23:46:02 +00:00
perf(CollectionField): use custom RecursionField component to avoid unnecessary re-renders
This commit is contained in:
parent
459c617309
commit
203ecc1334
@ -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} />;
|
||||
};
|
||||
|
148
packages/core/client/src/formily/NocoBaseRecursionField.tsx
Normal file
148
packages/core/client/src/formily/NocoBaseRecursionField.tsx
Normal 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';
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user