mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-28 05:05:48 +08:00
feat: use hooks to memo config-provider config
This commit is contained in:
parent
91c4cb2793
commit
81129554ce
53
components/_util/__tests__/useDeepMemo.test.js
Normal file
53
components/_util/__tests__/useDeepMemo.test.js
Normal 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');
|
||||
});
|
||||
});
|
23
components/_util/hooks/useDeepMemo.ts
Normal file
23
components/_util/hooks/useDeepMemo.ts
Normal 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;
|
||||
}
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user