feat: use hooks to memo config-provider config

This commit is contained in:
xinhui.zxh 2021-01-12 10:40:15 +08:00
parent 91c4cb2793
commit 81129554ce
4 changed files with 229 additions and 97 deletions

View File

@ -0,0 +1,53 @@
import React from 'react';
import { mount } from 'enzyme';
import useDeepMemo from '../hooks/useDeepMemo';
describe('useDeepMemo', () => {
it('should memo value when dep change immutably', () => {
const Test = () => {
const [dep, setDep] = React.useState({ count: 1 });
const memoedVal = useDeepMemo(() => dep.count + 1, dep);
return (
<span
onClick={() => {
setDep({ count: 1 });
}}
>
{memoedVal}
</span>
);
};
const wrapper = mount(<Test />);
expect(wrapper.text()).toEqual('2');
wrapper.find('span').simulate('click');
expect(wrapper.text()).toEqual('2');
});
it('should update memoed value when dep change mutably(using clone: true)', () => {
const Test = () => {
const [, forceRender] = React.useReducer(v => v + 1, 1);
const dep = React.useRef({ count: 1 });
const memoedVal = useDeepMemo(() => dep.current.count + 1, dep.current, { clone: true });
return (
<span
onClick={() => {
dep.current.count = 2;
forceRender();
}}
>
{memoedVal}
</span>
);
};
const wrapper = mount(<Test />);
expect(wrapper.text()).toEqual('2');
wrapper.find('span').simulate('click');
expect(wrapper.text()).toEqual('3');
});
});

View File

@ -0,0 +1,23 @@
import * as React from 'react';
import isEqual from 'lodash/isEqual';
import cloneDeep from 'lodash/cloneDeep';
export default function useDeepMemo<T>(
memoFn: () => T,
dep: any,
options: { clone: boolean } = {
clone: false,
},
) {
const { clone } = options;
const ref = React.useRef<T>();
if (!ref.current || !isEqual(dep, ref.current)) {
let val = memoFn();
if (clone) {
val = cloneDeep(val);
}
ref.current = val;
}
return ref.current;
}

View File

@ -1,6 +1,5 @@
import React from 'react';
import { mount } from 'enzyme';
import zhCN from '../../locale/zh_CN';
import ConfigProvider from '..';
import { ConfigConsumer } from '../context';
@ -19,11 +18,11 @@ describe('ConfigProvider', () => {
it('should not generate new value for same config', () => {
const MemoedSibling = React.memo(Sibling);
const App = () => {
const [count, setCount] = React.useState(0);
const [, forceRender] = React.useReducer(v => v + 1, 1);
return (
<ConfigProvider locale={zhCN}>
<button type="button" onClick={() => setCount(1)}>
{count}
<ConfigProvider pageHeader={{ ghost: true }} direction="ltr">
<button type="button" onClick={() => forceRender()}>
Force Render
</button>
<MemoedSibling />
</ConfigProvider>
@ -35,6 +34,27 @@ describe('ConfigProvider', () => {
expect(wrapper.find('#counter').text()).toEqual('1');
});
it('should not generate new value for same config in nested ConfigProvider', () => {
const MemoedSibling = React.memo(Sibling);
const App = () => {
const [, forceRender] = React.useReducer(v => v + 1, 1);
return (
<ConfigProvider pageHeader={{ ghost: true }} direction="ltr">
<ConfigProvider>
<button type="button" onClick={() => forceRender()}>
Force Render
</button>
<MemoedSibling />
</ConfigProvider>
</ConfigProvider>
);
};
const wrapper = mount(<App />);
wrapper.find('button').simulate('click');
expect(wrapper.find('#counter').text()).toEqual('1');
});
it('should rerender when context value change(immutable or mutable)', () => {
const MemoedSibling = React.memo(Sibling);
const App = () => {
@ -68,4 +88,44 @@ describe('ConfigProvider', () => {
wrapper.find('.mutable').simulate('click');
expect(wrapper.find('#counter').text()).toEqual('3');
});
it('should rerender when context value change(immutable or mutable) in nested ConfigProvider', () => {
const MemoedSibling = React.memo(Sibling);
const App = () => {
const [input, setInput] = React.useState({ autoComplete: 'true' });
const [, forceRender] = React.useReducer(v => v + 1, 1);
const pageHeader = React.useRef({ ghost: false });
return (
<ConfigProvider input={input} pageHeader={pageHeader.current}>
<ConfigProvider>
<button
type="button"
onClick={() => setInput({ autoComplete: 'false' })}
className="immutable"
>
Change Input
</button>
<button
type="button"
onClick={() => {
pageHeader.current.ghost = true;
forceRender();
}}
className="mutable"
>
Change Direction
</button>
<MemoedSibling />
</ConfigProvider>
</ConfigProvider>
);
};
const wrapper = mount(<App />);
wrapper.find('.immutable').simulate('click');
expect(wrapper.find('#counter').text()).toEqual('2');
wrapper.find('.mutable').simulate('click');
expect(wrapper.find('#counter').text()).toEqual('3');
});
});

View File

@ -1,11 +1,7 @@
// TODO: remove this lint
// SFC has specified a displayName, but not worked.
/* eslint-disable react/display-name */
import * as React from 'react';
import { FormProvider as RcFormProvider } from 'rc-field-form';
import { ValidateMessages } from 'rc-field-form/lib/interface';
import isEqual from 'lodash/isEqualWith';
import cloneDeep from 'lodash/cloneDeep';
import useDeepMemo from '../_util/hooks/useDeepMemo';
import { RenderEmptyHandler } from './renderEmpty';
import LocaleProvider, { Locale, ANT_MARK } from '../locale-provider';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
@ -70,23 +66,31 @@ export interface ConfigProviderProps {
dropdownMatchSelectWidth?: boolean;
}
const ConfigProvider: React.FC<ConfigProviderProps> & {
ConfigContext: typeof ConfigContext;
} = props => {
const lastConfigCloned = React.useRef<ConfigConsumerProps>();
const lastConfigRef = React.useRef<ConfigConsumerProps>();
const lastContextRef = React.useRef<ConfigConsumerProps>();
interface ProviderChildrenProps extends ConfigProviderProps {
parentContext: ConfigConsumerProps;
legacyLocale: Locale;
}
React.useEffect(() => {
if (props.direction) {
message.config({
rtl: props.direction === 'rtl',
});
notification.config({
rtl: props.direction === 'rtl',
});
}
}, [props.direction]);
const ProviderChildren: React.FC<ProviderChildrenProps> = props => {
const {
children,
getTargetContainer,
getPopupContainer,
renderEmpty,
csp,
autoInsertSpaceInButton,
form,
input,
locale,
pageHeader,
componentSize,
direction,
space,
virtual,
dropdownMatchSelectWidth,
legacyLocale,
parentContext,
} = props;
const getPrefixClsWrapper = (context: ConfigConsumerProps) => (
suffixCls: string,
@ -101,27 +105,13 @@ const ConfigProvider: React.FC<ConfigProviderProps> & {
return suffixCls ? `${mergedPrefixCls}-${suffixCls}` : mergedPrefixCls;
};
const renderProvider = (context: ConfigConsumerProps, legacyLocale: Locale) => {
const {
children,
getTargetContainer,
getPopupContainer,
renderEmpty,
csp,
autoInsertSpaceInButton,
form,
input,
locale,
pageHeader,
componentSize,
direction,
space,
virtual,
dropdownMatchSelectWidth,
} = props;
const getPrefixCls = React.useMemo(() => getPrefixClsWrapper(parentContext), [
parentContext.getPrefixCls,
]);
let config: ConfigConsumerProps = {
...context,
const getConfig = (): ConfigConsumerProps => {
const config = {
...parentContext,
csp,
autoInsertSpaceInButton,
locale: locale || legacyLocale,
@ -129,20 +119,8 @@ const ConfigProvider: React.FC<ConfigProviderProps> & {
space,
virtual,
dropdownMatchSelectWidth,
getPrefixCls,
};
const lastContext = lastContextRef.current;
const lastConfig = lastConfigRef.current;
// Only get new getPrefixCls when context change
if ((lastContext && lastContext.getPrefixCls !== context.getPrefixCls) || !lastContext) {
config.getPrefixCls = getPrefixClsWrapper(context);
} else if (lastConfig) {
config.getPrefixCls = lastConfig.getPrefixCls;
}
lastContextRef.current = context;
if (getTargetContainer) {
config.getTargetContainer = getTargetContainer;
}
@ -167,50 +145,68 @@ const ConfigProvider: React.FC<ConfigProviderProps> & {
config.form = form;
}
let childNode = children;
// Additional Form provider
let validateMessages: ValidateMessages = {};
if (locale && locale.Form && locale.Form.defaultValidateMessages) {
validateMessages = locale.Form.defaultValidateMessages;
}
if (form && form.validateMessages) {
validateMessages = { ...validateMessages, ...form.validateMessages };
}
if (Object.keys(validateMessages).length > 0) {
childNode = <RcFormProvider validateMessages={validateMessages}>{children}</RcFormProvider>;
}
const childrenWithLocale =
locale === undefined ? (
childNode
) : (
<LocaleProvider locale={locale || legacyLocale} _ANT_MARK__={ANT_MARK}>
{childNode}
</LocaleProvider>
);
// https://github.com/ant-design/ant-design/issues/27617
if (lastConfig && isEqual(config, lastConfigCloned.current)) {
config = lastConfig;
} else {
lastConfigCloned.current = cloneDeep(config);
lastConfigRef.current = config;
}
return (
<SizeContextProvider size={componentSize}>
<ConfigContext.Provider value={config}>{childrenWithLocale}</ConfigContext.Provider>
</SizeContextProvider>
);
return config;
};
// https://github.com/ant-design/ant-design/issues/27617
const memoedConfig = useDeepMemo(() => getConfig(), getConfig(), { clone: true });
let childNode = children;
// Additional Form provider
let validateMessages: ValidateMessages = {};
if (locale && locale.Form && locale.Form.defaultValidateMessages) {
validateMessages = locale.Form.defaultValidateMessages;
}
if (form && form.validateMessages) {
validateMessages = { ...validateMessages, ...form.validateMessages };
}
if (Object.keys(validateMessages).length > 0) {
childNode = <RcFormProvider validateMessages={validateMessages}>{children}</RcFormProvider>;
}
const childrenWithLocale =
locale === undefined ? (
childNode
) : (
<LocaleProvider locale={locale || legacyLocale} _ANT_MARK__={ANT_MARK}>
{childNode}
</LocaleProvider>
);
return (
<SizeContextProvider size={componentSize}>
<ConfigContext.Provider value={memoedConfig}>{childrenWithLocale}</ConfigContext.Provider>
</SizeContextProvider>
);
};
const ConfigProvider: React.FC<ConfigProviderProps> & {
ConfigContext: typeof ConfigContext;
} = props => {
React.useEffect(() => {
if (props.direction) {
message.config({
rtl: props.direction === 'rtl',
});
notification.config({
rtl: props.direction === 'rtl',
});
}
}, [props.direction]);
return (
<LocaleReceiver>
{(_, __, legacyLocale) => (
<ConfigConsumer>
{context => renderProvider(context, legacyLocale as Locale)}
{context => (
<ProviderChildren
parentContext={context}
legacyLocale={legacyLocale as Locale}
{...props}
/>
)}
</ConfigConsumer>
)}
</LocaleReceiver>