From b3ef405b2e1fcfe688f98096520a828b5d098ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=9F=E8=B4=A4?= Date: Mon, 18 Sep 2023 15:57:33 +0800 Subject: [PATCH] fix(table): less render function --- .../field/src/components/Select/index.tsx | 14 +- packages/field/src/index.tsx | 39 +--- packages/form/src/BaseForm/createField.tsx | 62 +++---- .../form/src/components/FormItem/index.tsx | 59 ++++-- .../form/src/components/SchemaForm/index.tsx | 175 +++++++++--------- .../form/src/components/SchemaForm/typing.ts | 4 +- .../src/hooks/useDeepCompareEffect/index.ts | 2 +- .../src/hooks/useDeepCompareMemo/index.ts | 19 ++ packages/utils/src/index.tsx | 2 + tests/form/schemaForm.test.tsx | 22 +-- tests/table/valueEnum.test.tsx | 13 +- 11 files changed, 212 insertions(+), 199 deletions(-) create mode 100644 packages/utils/src/hooks/useDeepCompareMemo/index.ts diff --git a/packages/field/src/components/Select/index.tsx b/packages/field/src/components/Select/index.tsx index e3027f0f9db5..1f8fca1745ba 100644 --- a/packages/field/src/components/Select/index.tsx +++ b/packages/field/src/components/Select/index.tsx @@ -1,23 +1,22 @@ import { useIntl } from '@ant-design/pro-provider'; -import type { +import { + nanoid, ProFieldRequestData, ProFieldValueEnumType, ProSchemaValueEnumMap, ProSchemaValueEnumObj, RequestOptionsType, -} from '@ant-design/pro-utils'; -import { - nanoid, useDebounceValue, useDeepCompareEffect, + useDeepCompareMemo, useMountMergeState, + useRefFunction, useStyle, } from '@ant-design/pro-utils'; import type { SelectProps } from 'antd'; import { ConfigProvider, Space, Spin } from 'antd'; import type { ReactNode } from 'react'; import React, { - useCallback, useContext, useEffect, useImperativeHandle, @@ -324,7 +323,7 @@ export const useFieldFetchData = ( const proFieldKeyRef = useRef(cacheKey); - const getOptionsFormValueEnum = useCallback( + const getOptionsFormValueEnum = useRefFunction( (coverValueEnum: ProFieldValueEnumType) => { return proFieldParsingValueEnumToArray(ObjToMap(coverValueEnum)).map( ({ value, text, ...rest }) => ({ @@ -335,10 +334,9 @@ export const useFieldFetchData = ( }), ); }, - [], ); - const defaultOptions = useMemo(() => { + const defaultOptions = useDeepCompareMemo(() => { if (!fieldProps) return undefined; const data = fieldProps?.options || fieldProps?.treeData; if (!data) return undefined; diff --git a/packages/field/src/index.tsx b/packages/field/src/index.tsx index e36c8e2d64ad..1e9dc9a6f3bb 100644 --- a/packages/field/src/index.tsx +++ b/packages/field/src/index.tsx @@ -4,20 +4,18 @@ import type { ProRenderFieldPropsType, } from '@ant-design/pro-provider'; import ProConfigContext from '@ant-design/pro-provider'; -import type { +import { + omitUndefined, + pickProProps, ProFieldRequestData, ProFieldTextType, ProFieldValueObjectType, ProFieldValueType, -} from '@ant-design/pro-utils'; -import { - omitUndefined, - pickProProps, + useDeepCompareMemo, useRefFunction, } from '@ant-design/pro-utils'; import { Avatar } from 'antd'; -import { noteOnce } from 'rc-util/lib/warning'; -import React, { useContext, useMemo } from 'react'; +import React, { useContext } from 'react'; import FieldCascader from './components/Cascader'; import FieldCheckbox from './components/Checkbox'; import FieldCode from './components/Code'; @@ -240,28 +238,6 @@ const defaultRenderText = ( } } - const needValueEnum = REQUEST_VALUE_TYPE.includes(valueType as string); - const hasValueEnum = !!( - props.valueEnum || - props.request || - props.options || - props.fieldProps?.options - ); - - noteOnce( - !needValueEnum || hasValueEnum, - `如果设置了 valueType 为 ${REQUEST_VALUE_TYPE.join( - ',', - )}中任意一个,则需要配置options,request, valueEnum 其中之一,否则无法生成选项。`, - ); - - noteOnce( - !needValueEnum || hasValueEnum, - `If you set valueType to any of ${REQUEST_VALUE_TYPE.join( - ',', - )}, you need to configure options, request or valueEnum.`, - ); - /** 如果是金额的值 */ if (valueType === 'money') { return ; @@ -645,7 +621,7 @@ const ProFieldComponent: React.ForwardRefRenderFunction< onChange?.(...restParams); }); - const fieldProps: any = useMemo(() => { + const fieldProps: any = useDeepCompareMemo(() => { return ( (value !== undefined || restFieldProps) && { value, @@ -657,7 +633,7 @@ const ProFieldComponent: React.ForwardRefRenderFunction< // eslint-disable-next-line react-hooks/exhaustive-deps }, [value, restFieldProps, onChangeCallBack]); - const renderedDom = useMemo(() => { + const renderedDom = useDeepCompareMemo(() => { return defaultRenderText( mode === 'edit' ? fieldProps?.value ?? text ?? '' @@ -706,6 +682,7 @@ const ProFieldComponent: React.ForwardRefRenderFunction< text, valueType, ]); + return {renderedDom}; }; diff --git a/packages/form/src/BaseForm/createField.tsx b/packages/form/src/BaseForm/createField.tsx index 3877481d9f17..b2421c34bcb3 100644 --- a/packages/form/src/BaseForm/createField.tsx +++ b/packages/form/src/BaseForm/createField.tsx @@ -1,15 +1,16 @@ import { - isDeepEqualReact, omitUndefined, pickProFormItemProps, stringify, + useDeepCompareMemo, usePrevious, + useRefFunction, } from '@ant-design/pro-utils'; import type { FormItemProps } from 'antd'; import classnames from 'classnames'; import { FieldContext as RcFieldContext } from 'rc-field-form'; import { noteOnce } from 'rc-util/lib/warning'; -import React, { useCallback, useContext, useMemo, useState } from 'react'; +import React, { useContext, useMemo, useState } from 'react'; import { ProFormDependency, ProFormItem } from '../components'; import FieldContext from '../FieldContext'; import { useGridHelpers } from '../helpers'; @@ -118,7 +119,7 @@ function createField

( /** * dependenciesValues change to trigger re-execute of getFieldProps and getFormItemProps */ - const changedProps = useMemo( + const changedProps = useDeepCompareMemo( () => { return { formItemProps: getFormItemProps?.(), @@ -130,7 +131,7 @@ function createField

( [getFieldProps, getFormItemProps, rest.dependenciesValues, onlyChange], ); - const fieldProps: Record = useMemo(() => { + const fieldProps: Record = useDeepCompareMemo(() => { const newFieldProps: any = { ...(ignoreFormItem ? omitUndefined({ value: rest.value }) : {}), placeholder, @@ -157,7 +158,7 @@ function createField

( // restFormItemProps is user props pass to Form.Item const restFormItemProps = pickProFormItemProps(rest); - const formItemProps: FormItemProps = useMemo( + const formItemProps: FormItemProps = useDeepCompareMemo( () => ({ ...contextValue.formItemProps, ...restFormItemProps, @@ -174,7 +175,7 @@ function createField

( ], ); - const otherProps = useMemo( + const otherProps = useDeepCompareMemo( () => ({ messageVariables, ...defaultFormItemProps, @@ -190,7 +191,8 @@ function createField

( ); const { prefixName } = useContext(RcFieldContext); - const proFieldKey = useMemo(() => { + + const proFieldKey = useDeepCompareMemo(() => { let name = otherProps?.name; if (Array.isArray(name)) name = name.join('_'); if (Array.isArray(prefixName) && name) @@ -203,19 +205,16 @@ function createField

( const prefRest = usePrevious(rest); - const onChange = useCallback( - (...restParams: any[]) => { - if (getFormItemProps || getFieldProps) { - forceUpdateByOnChange([]); - } else if (rest.renderFormItem) { - forceUpdate([]); - } - fieldProps?.onChange?.(...restParams); - }, - [getFieldProps, getFormItemProps, fieldProps, rest.renderFormItem], - ); + const onChange = useRefFunction((...restParams: any[]) => { + if (getFormItemProps || getFieldProps) { + forceUpdateByOnChange([]); + } else if (rest.renderFormItem) { + forceUpdate([]); + } + fieldProps?.onChange?.(...restParams); + }); - const style = useMemo(() => { + const style = useDeepCompareMemo(() => { const newStyle = { width: width && !WIDTH_SIZE_ENUM[width] @@ -232,7 +231,7 @@ function createField

( // eslint-disable-next-line react-hooks/exhaustive-deps }, [stringify(fieldProps?.style), contextValue.grid, isIgnoreWidth, width]); - const className = useMemo(() => { + const className = useDeepCompareMemo(() => { const isSizeEnum = width && WIDTH_SIZE_ENUM[width]; return ( classnames(fieldProps?.className, { @@ -242,7 +241,7 @@ function createField

( ); }, [width, fieldProps?.className, isIgnoreWidth]); - const fieldProFieldProps = useMemo(() => { + const fieldProFieldProps = useDeepCompareMemo(() => { return omitUndefined({ ...contextValue.proFieldProps, mode: rest?.mode, @@ -262,7 +261,7 @@ function createField

( proFieldProps, ]); - const fieldFieldProps = useMemo(() => { + const fieldFieldProps = useDeepCompareMemo(() => { return { onChange, allowClear, @@ -271,7 +270,8 @@ function createField

( className, }; }, [allowClear, className, onChange, fieldProps, style]); - const field = useMemo(() => { + + const field = useDeepCompareMemo(() => { return ( ( /> ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - fieldProFieldProps, - fieldFieldProps, - // eslint-disable-next-line react-hooks/exhaustive-deps - isDeepEqualReact(prefRest, rest, [ - 'onChange', - 'onBlur', - 'onFocus', - 'record', - ]) - ? undefined - : {}, - ]); + }, [fieldProFieldProps, fieldFieldProps, rest]); // 使用useMemo包裹避免不必要的re-render - const formItem = useMemo(() => { + const formItem = useDeepCompareMemo(() => { return ( )?.fieldProps?.onChange?.( ...restParams, ); }); const onBlurMemo = useRefFunction(function (...restParams: any[]): void { - // @ts-ignore - if (filedChildren?.type?.displayName !== 'ProFormComponent') return; - if (!React.isValidElement(filedChildren)) return; + if (isProFormComponent) return; + if (isValidElementForFiledChildren) return; onBlur?.(...restParams); filedChildren?.props?.onBlur?.(...restParams); (filedChildren?.props as Record)?.fieldProps?.onBlur?.( @@ -64,26 +69,40 @@ const WithValueFomFiledProps: React.FC< ); }); - const fieldProps = useMemo(() => { - // @ts-ignore - if (filedChildren?.type?.displayName !== 'ProFormComponent') - return undefined; - if (!React.isValidElement(filedChildren)) return undefined; + const omitOnBlurAndOnChangeProps = useDeepCompareMemo( + () => + omit( + // @ts-ignore + filedChildren?.props?.fieldProps || {}, + ['onBlur', 'onChange'], + ), + [ + omit( + // @ts-ignore + filedChildren?.props?.fieldProps || {}, + ['onBlur', 'onChange'], + ), + ], + ); + const propsValuePropName = formFieldProps[valuePropName]; + const fieldProps = useMemo(() => { + if (isProFormComponent) return undefined; + if (isValidElementForFiledChildren) return undefined; return omitUndefined({ id: restProps.id, // 优先使用 children.props.fieldProps, // 比如 LightFilter 中可能需要通过 fieldProps 覆盖 Form.Item 默认的 onChange - [valuePropName]: formFieldProps[valuePropName], - ...(filedChildren?.props?.fieldProps || {}), + [valuePropName]: propsValuePropName, + ...omitOnBlurAndOnChangeProps, onBlur: onBlurMemo, // 这个 onChange 是 Form.Item 添加上的, // 要通过 fieldProps 透传给 ProField 调用 onChange: onChangeMemo, }); }, [ - filedChildren, - formFieldProps, + propsValuePropName, + omitOnBlurAndOnChangeProps, onBlurMemo, onChangeMemo, restProps.id, diff --git a/packages/form/src/components/SchemaForm/index.tsx b/packages/form/src/components/SchemaForm/index.tsx index 87b7c2e86cab..6a41744b1cd2 100644 --- a/packages/form/src/components/SchemaForm/index.tsx +++ b/packages/form/src/components/SchemaForm/index.tsx @@ -3,15 +3,16 @@ omitUndefined, runFunction, stringify, + useDeepCompareMemo, useLatest, useReactiveRef, + useRefFunction, } from '@ant-design/pro-utils'; import type { FormProps } from 'antd'; import { Form } from 'antd'; import React, { useCallback, useImperativeHandle, - useMemo, useRef, useState, } from 'react'; @@ -84,91 +85,88 @@ function BetaSchemaForm( * @param items */ const genItems: ProFormRenderValueTypeHelpers['genItems'] = - useCallback( - (items: ProFormColumnsType[]) => { - return items - .filter((originItem) => { - return !(originItem.hideInForm && type === 'form'); - }) - .sort((a, b) => { - if (b.order || a.order) { - return (b.order || 0) - (a.order || 0); - } - return (b.index || 0) - (a.index || 0); - }) - .map((originItem, index) => { - const title = runFunction( - originItem.title, - originItem, - 'form', - , - ); - - const item = omitUndefined({ - title, - label: title, - name: originItem.name, - valueType: runFunction(originItem.valueType, {}), - key: originItem.key || originItem.dataIndex || index, - columns: originItem.columns, - valueEnum: originItem.valueEnum, - dataIndex: originItem.dataIndex || originItem.key, - initialValue: originItem.initialValue, - width: originItem.width, - index: originItem.index, - readonly: originItem.readonly, - colSize: originItem.colSize, - colProps: originItem.colProps, - rowProps: originItem.rowProps, - className: originItem.className, - tooltip: originItem.tooltip || originItem.tip, - dependencies: originItem.dependencies, - proFieldProps: originItem.proFieldProps, - ignoreFormItem: originItem.ignoreFormItem, - getFieldProps: originItem.fieldProps - ? () => - runFunction( - originItem.fieldProps, - formRef.current, - originItem, - ) - : undefined, - getFormItemProps: originItem.formItemProps - ? () => - runFunction( - originItem.formItemProps, - formRef.current, - originItem, - ) - : undefined, - render: originItem.render, - renderFormItem: originItem.renderFormItem, - renderText: originItem.renderText, - request: originItem.request, - params: originItem.params, - transform: originItem.transform, - convertValue: originItem.convertValue, - debounceTime: originItem.debounceTime, - defaultKeyWords: originItem.defaultKeyWords, - }) as ItemType; - - return renderValueType(item, { - action, - type, - originItem, - formRef, - genItems, - }); - }) - .filter((field) => { - return Boolean(field); + useRefFunction((items: ProFormColumnsType[]) => { + return items + .filter((originItem) => { + return !(originItem.hideInForm && type === 'form'); + }) + .sort((a, b) => { + if (b.order || a.order) { + return (b.order || 0) - (a.order || 0); + } + return (b.index || 0) - (a.index || 0); + }) + .map((originItem, index) => { + const title = runFunction( + originItem.title, + originItem, + 'form', + , + ); + + const item = omitUndefined({ + title, + label: title, + name: originItem.name, + valueType: runFunction(originItem.valueType, {}), + key: originItem.key || originItem.dataIndex || index, + columns: originItem.columns, + valueEnum: originItem.valueEnum, + dataIndex: originItem.dataIndex || originItem.key, + initialValue: originItem.initialValue, + width: originItem.width, + index: originItem.index, + readonly: originItem.readonly, + colSize: originItem.colSize, + colProps: originItem.colProps, + rowProps: originItem.rowProps, + className: originItem.className, + tooltip: originItem.tooltip || originItem.tip, + dependencies: originItem.dependencies, + proFieldProps: originItem.proFieldProps, + ignoreFormItem: originItem.ignoreFormItem, + getFieldProps: originItem.fieldProps + ? () => + runFunction( + originItem.fieldProps, + formRef.current, + originItem, + ) + : undefined, + getFormItemProps: originItem.formItemProps + ? () => + runFunction( + originItem.formItemProps, + formRef.current, + originItem, + ) + : undefined, + render: originItem.render, + renderFormItem: originItem.renderFormItem, + renderText: originItem.renderText, + request: originItem.request, + params: originItem.params, + transform: originItem.transform, + convertValue: originItem.convertValue, + debounceTime: originItem.debounceTime, + defaultKeyWords: originItem.defaultKeyWords, + }) as ItemType; + + return renderValueType(item, { + action, + type, + originItem, + formRef, + genItems, }); - }, - [action, !!formRef.current, type], - ); + }) + .filter((field) => { + return Boolean(field); + }); + }); const onValuesChange: FormProps['onValuesChange'] = useCallback( (changedValues: any, values: T) => { @@ -185,18 +183,19 @@ function BetaSchemaForm( }, [propsRef, shouldUpdate], ); - const formChildrenDoms = useMemo(() => { + + const formChildrenDoms = useDeepCompareMemo(() => { if (!formRef.current) return; // like StepsForm's columns but not only for StepsForm if (columns.length && Array.isArray(columns[0])) return; return genItems(columns as ProFormColumnsType[]); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [columns, genItems, formDomsDeps, !!formRef.current]); + }, [columns, restProps?.open, action, type, formDomsDeps, !!formRef.current]); /** * Append layoutType component specific props */ - const specificProps = useMemo(() => { + const specificProps = useDeepCompareMemo(() => { if (layoutType === 'StepsForm') { return { forceUpdate: forceUpdate, diff --git a/packages/form/src/components/SchemaForm/typing.ts b/packages/form/src/components/SchemaForm/typing.ts index f398dcfeeb83..4b9047d5d6ce 100644 --- a/packages/form/src/components/SchemaForm/typing.ts +++ b/packages/form/src/components/SchemaForm/typing.ts @@ -141,7 +141,9 @@ export type FormSchema, ValueType = 'text'> = { shouldUpdate?: boolean | ((newValues: T, oldValues?: T) => boolean); } & Omit, 'onFinish'> & ProFormPropsType & - CommonFormProps; + CommonFormProps & { + open?: boolean; + }; export type ProFormRenderValueTypeItem = { label: any; diff --git a/packages/utils/src/hooks/useDeepCompareEffect/index.ts b/packages/utils/src/hooks/useDeepCompareEffect/index.ts index ce37c5ec2abf..d18b4ae051ce 100644 --- a/packages/utils/src/hooks/useDeepCompareEffect/index.ts +++ b/packages/utils/src/hooks/useDeepCompareEffect/index.ts @@ -6,7 +6,7 @@ import { useDebounceFn } from '../useDebounceFn'; export const isDeepEqual = (a: any, b: any, ignoreKeys?: string[]) => isDeepEqualReact(a, b, ignoreKeys); -function useDeepCompareMemoize(value: any, ignoreKeys?: string[]) { +export function useDeepCompareMemoize(value: any, ignoreKeys?: string[]) { const ref = useRef(); // it can be done by using useMemo as well // but useRef is rather cleaner and easier diff --git a/packages/utils/src/hooks/useDeepCompareMemo/index.ts b/packages/utils/src/hooks/useDeepCompareMemo/index.ts new file mode 100644 index 000000000000..01dbdb7968db --- /dev/null +++ b/packages/utils/src/hooks/useDeepCompareMemo/index.ts @@ -0,0 +1,19 @@ +import React from 'react'; +import { useDeepCompareMemoize } from '../useDeepCompareEffect'; + +/** + * `useDeepCompareMemo` will only recompute the memoized value when one of the + * `deps` has changed. + * + * Usage note: only use this if `deps` are objects or arrays that contain + * objects. Otherwise you should just use React.useMemo. + * + */ +function useDeepCompareMemo( + factory: () => T, + dependencies: React.DependencyList, +) { + return React.useMemo(factory, useDeepCompareMemoize(dependencies)); +} + +export default useDeepCompareMemo; diff --git a/packages/utils/src/index.tsx b/packages/utils/src/index.tsx index 3ecb0553e90c..f1aaba548ebd 100644 --- a/packages/utils/src/index.tsx +++ b/packages/utils/src/index.tsx @@ -32,6 +32,7 @@ import { useDeepCompareEffect, useDeepCompareEffectDebounce, } from './hooks/useDeepCompareEffect'; +import useDeepCompareMemo from './hooks/useDeepCompareMemo'; import { useDocumentTitle } from './hooks/useDocumentTitle'; import type { ProRequestData } from './hooks/useFetchData'; import { useFetchData } from './hooks/useFetchData'; @@ -147,4 +148,5 @@ export { useReactiveRef, useRefCallback, useBreakpoint, + useDeepCompareMemo, }; diff --git a/tests/form/schemaForm.test.tsx b/tests/form/schemaForm.test.tsx index 9530d6c6d872..585ee9372b0a 100644 --- a/tests/form/schemaForm.test.tsx +++ b/tests/form/schemaForm.test.tsx @@ -123,7 +123,7 @@ describe('SchemaForm', () => { await waitFor(() => { expect(requestFn).toBeCalledWith('qixian'); expect(formItemPropsFn).toBeCalledTimes(2); - expect(fieldPropsFn).toBeCalledTimes(4); + expect(fieldPropsFn).toBeCalledTimes(2); }); }); @@ -174,9 +174,9 @@ describe('SchemaForm', () => { }); await waitFor(() => { - expect(renderFormItemFn).toBeCalledTimes(6); - expect(fieldPropsFn).toBeCalledTimes(2); - expect(formItemPropsFn).toBeCalledTimes(2); + expect(renderFormItemFn).toBeCalledTimes(5); + expect(fieldPropsFn).toBeCalledTimes(1); + expect(formItemPropsFn).toBeCalledTimes(1); expect(onValuesChangeFn).toBeCalled(); }); }); @@ -234,8 +234,8 @@ describe('SchemaForm', () => { await waitFor(() => { expect(shouldUpdateFn).toBeCalledTimes(0); - expect(fieldPropsFn).toBeCalledTimes(3); - expect(formItemPropsFn).toBeCalledTimes(3); + expect(fieldPropsFn).toBeCalledTimes(1); + expect(formItemPropsFn).toBeCalledTimes(1); expect(renderFormItemFn).toBeCalledTimes(4); }); @@ -247,8 +247,8 @@ describe('SchemaForm', () => { // Although shouldUpdate returns false, but using dependencies will still update await waitFor(() => { expect(renderFormItemFn).toBeCalledTimes(5); - expect(formItemPropsFn).toBeCalledTimes(4); - expect(fieldPropsFn).toBeCalledTimes(4); + expect(formItemPropsFn).toBeCalledTimes(2); + expect(fieldPropsFn).toBeCalledTimes(2); expect(shouldUpdateFn).toBeCalledTimes(1); }); @@ -259,9 +259,9 @@ describe('SchemaForm', () => { }); await waitFor(() => { - expect(renderFormItemFn).toBeCalledTimes(7); - expect(formItemPropsFn).toBeCalledTimes(5); - expect(fieldPropsFn).toBeCalledTimes(5); + expect(renderFormItemFn).toBeCalledTimes(6); + expect(formItemPropsFn).toBeCalledTimes(3); + expect(fieldPropsFn).toBeCalledTimes(3); expect(shouldUpdateFn).toBeCalledTimes(2); expect(shouldUpdateFn).toBeCalledWith(true); }); diff --git a/tests/table/valueEnum.test.tsx b/tests/table/valueEnum.test.tsx index 2ffc2c058ff4..f3d6c4a65c0c 100644 --- a/tests/table/valueEnum.test.tsx +++ b/tests/table/valueEnum.test.tsx @@ -74,7 +74,9 @@ describe('Table valueEnum', () => { rowKey="key" />, ); - await waitForWaitTime(1200); + await waitFor(() => { + return html.findAllByText('2'); + }); act(() => { html.rerender( @@ -108,7 +110,11 @@ describe('Table valueEnum', () => { />, ); }); - await waitForWaitTime(200); + + await waitFor(() => { + return html.findAllByText('已上线'); + }); + act(() => { html.baseElement .querySelector('form.ant-form div.ant-select') @@ -121,6 +127,9 @@ describe('Table valueEnum', () => { )?.textContent, ).toBe('01关闭运行中已上线异常'); }); + + console.log(html.baseElement.querySelector('table')?.innerHTML); + expect( html.baseElement.querySelector('td.ant-table-cell') ?.textContent,