diff --git a/components/_util/ActionButton.tsx b/components/_util/ActionButton.tsx index 7c88bfcfd1..50540b3c55 100644 --- a/components/_util/ActionButton.tsx +++ b/components/_util/ActionButton.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import Button from '../button'; import { LegacyButtonType, ButtonProps, convertLegacyProps } from '../button/button'; +import useDestroyed from './hooks/useDestroyed'; export interface ActionButtonProps { type?: LegacyButtonType; @@ -20,6 +21,7 @@ function isThenable(thing?: PromiseLike): boolean { const ActionButton: React.FC = props => { const clickedRef = React.useRef(false); const ref = React.useRef(); + const isDestroyed = useDestroyed(); const [loading, setLoading] = React.useState(false); React.useEffect(() => { @@ -43,7 +45,9 @@ const ActionButton: React.FC = props => { setLoading(true); returnValueOfOnOk!.then( (...args: any[]) => { - setLoading(false); + if (!isDestroyed()) { + setLoading(false); + } close(...args); clickedRef.current = false; }, @@ -52,7 +56,9 @@ const ActionButton: React.FC = props => { // eslint-disable-next-line no-console console.error(e); // See: https://github.com/ant-design/ant-design/issues/6183 - setLoading(false); + if (!isDestroyed()) { + setLoading(false); + } clickedRef.current = false; }, ); diff --git a/components/_util/__tests__/useDestroyed.test.js b/components/_util/__tests__/useDestroyed.test.js new file mode 100644 index 0000000000..d98979e49f --- /dev/null +++ b/components/_util/__tests__/useDestroyed.test.js @@ -0,0 +1,20 @@ +import { mount } from 'enzyme'; +import React from 'react'; +import useDestroyed from '../hooks/useDestroyed'; + +describe('useMounted', () => { + it('should work properly', () => { + let isDestroyed = null; + + const AutoUnmounted = () => { + isDestroyed = useDestroyed(); + + return
Mounted
; + }; + + const wrapper = mount(); + expect(isDestroyed()).toBeFalsy(); + wrapper.unmount(); + expect(isDestroyed()).toBeTruthy(); + }); +}); diff --git a/components/_util/hooks/useDestroyed.ts b/components/_util/hooks/useDestroyed.ts new file mode 100644 index 0000000000..4b361a78ae --- /dev/null +++ b/components/_util/hooks/useDestroyed.ts @@ -0,0 +1,14 @@ +import * as React from 'react'; + +export default function useDestroyed() { + const mountedRef = React.useRef(true); + + React.useEffect( + () => () => { + mountedRef.current = false; + }, + [], + ); + + return () => !mountedRef.current; +} diff --git a/components/popconfirm/__tests__/index.test.js b/components/popconfirm/__tests__/index.test.js index f0698fb16d..f704e74310 100644 --- a/components/popconfirm/__tests__/index.test.js +++ b/components/popconfirm/__tests__/index.test.js @@ -5,6 +5,7 @@ import Popconfirm from '..'; import mountTest from '../../../tests/shared/mountTest'; import { sleep } from '../../../tests/utils'; import rtlTest from '../../../tests/shared/rtlTest'; +import Button from '../../button'; describe('Popconfirm', () => { mountTest(Popconfirm); @@ -223,4 +224,45 @@ describe('Popconfirm', () => { triggerNode.simulate('keydown', { key: 'Escape', keyCode: 27 }); expect(onVisibleChange).toHaveBeenLastCalledWith(false, eventObject); }); + + it('should not warn memory leaking if setState in async callback', async () => { + const error = jest.spyOn(console, 'error'); + + const Test = () => { + const [show, setShow] = React.useState(true); + + if (show) { + return ( + + new Promise(resolve => { + setTimeout(() => { + setShow(false); + resolve(); + }, 300); + }) + } + > + + + ); + } + return ; + }; + + const wrapper = mount( +
+ +
, + ); + + expect(wrapper.text()).toEqual('Test'); + const triggerNode = wrapper.find('.clickTarget').at(0); + triggerNode.simulate('click'); + wrapper.find('.ant-btn-primary').simulate('click'); + await sleep(500); + expect(wrapper.text()).toEqual('Unmounted'); + expect(error).not.toHaveBeenCalled(); + }); }); diff --git a/components/popconfirm/index.tsx b/components/popconfirm/index.tsx index 1537e84eda..24cffb7940 100644 --- a/components/popconfirm/index.tsx +++ b/components/popconfirm/index.tsx @@ -13,6 +13,7 @@ import { getRenderPropValue, RenderFunction } from '../_util/getRenderPropValue' import { cloneElement } from '../_util/reactNode'; import { getTransitionName } from '../_util/motion'; import ActionButton from '../_util/ActionButton'; +import useDestroyed from '../_util/hooks/useDestroyed'; export interface PopconfirmProps extends AbstractTooltipProps { title: React.ReactNode | RenderFunction; @@ -48,12 +49,15 @@ const Popconfirm = React.forwardRef((props, ref) => { defaultValue: props.defaultVisible, }); + const isDestroyed = useDestroyed(); + const settingVisible = ( value: boolean, e?: React.MouseEvent | React.KeyboardEvent, ) => { - setVisible(value); - + if (!isDestroyed()) { + setVisible(value); + } props.onVisibleChange?.(value, e); };