From 9174806a7997a45893a24d149027119f4a0709c3 Mon Sep 17 00:00:00 2001 From: Dhaya <154633+dhayab@users.noreply.github.com> Date: Thu, 13 Oct 2022 13:47:08 +0200 Subject: [PATCH] feat(core): support react 18 strict mode (#3653) --- .../src/core/__tests__/createConnector.js | 17 +++++-- .../src/core/createConnector.tsx | 48 +++++++++++-------- .../src/widgets/InstantSearch.tsx | 12 ++++- .../src/widgets/__tests__/InstantSearch.js | 6 ++- 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/packages/react-instantsearch-core/src/core/__tests__/createConnector.js b/packages/react-instantsearch-core/src/core/__tests__/createConnector.js index 35b6bd7c94..283d1cb582 100644 --- a/packages/react-instantsearch-core/src/core/__tests__/createConnector.js +++ b/packages/react-instantsearch-core/src/core/__tests__/createConnector.js @@ -5,6 +5,7 @@ import createConnector, { createConnectorWithoutContext, } from '../createConnector'; import { InstantSearchProvider } from '../context'; +import { wait } from '../../../../../test/utils'; Enzyme.configure({ adapter: new Adapter() }); @@ -415,7 +416,7 @@ describe('createConnector', () => { expect(subscribe).toHaveBeenCalledTimes(1); }); - it('unsubscribes from the store on unmount', () => { + it('unsubscribes from the store on unmount', async () => { const Connected = createConnectorWithoutContext({ displayName: 'Connector', getProvidedProps: () => {}, @@ -435,6 +436,8 @@ describe('createConnector', () => { wrapper.unmount(); + await wait(0); + expect(unsubscribe).toHaveBeenCalledTimes(1); }); @@ -787,7 +790,7 @@ describe('createConnector', () => { expect(onSearchStateChange).not.toHaveBeenCalled(); }); - it('unregisters itself on unmount', () => { + it('unregisters itself on unmount', async () => { const Connected = createConnectorWithoutContext({ displayName: 'Connector', getProvidedProps: () => {}, @@ -808,10 +811,12 @@ describe('createConnector', () => { wrapper.unmount(); + await wait(0); + expect(unregisterWidget).toHaveBeenCalledTimes(1); }); - it('calls onSearchStateChange with cleanUp on unmount', () => { + it('calls onSearchStateChange with cleanUp on unmount', async () => { const cleanUp = jest.fn(function (props, searchState) { return { instanceProps: this.props, @@ -858,6 +863,8 @@ describe('createConnector', () => { wrapper.unmount(); + await wait(0); + expect(cleanUp).toHaveBeenCalledTimes(1); expect(onSearchStateChange).toHaveBeenCalledTimes(1); expect(onSearchStateChange).toHaveBeenCalledWith({ @@ -875,7 +882,7 @@ describe('createConnector', () => { }); }); - it('calls onSearchStateChange with cleanUp without empty keys on unmount', () => { + it('calls onSearchStateChange with cleanUp without empty keys on unmount', async () => { const cleanUp = jest.fn((_, searchState) => searchState); const Connected = createConnectorWithoutContext({ @@ -910,6 +917,8 @@ describe('createConnector', () => { wrapper.unmount(); + await wait(0); + expect(onSearchStateChange).toHaveBeenCalledWith({ query: 'hello', }); diff --git a/packages/react-instantsearch-core/src/core/createConnector.tsx b/packages/react-instantsearch-core/src/core/createConnector.tsx index 0d75e122b6..381d7c73f9 100644 --- a/packages/react-instantsearch-core/src/core/createConnector.tsx +++ b/packages/react-instantsearch-core/src/core/createConnector.tsx @@ -102,6 +102,7 @@ export function createConnectorWithoutContext( unsubscribe?: () => void; unregisterWidget?: () => void; + cleanupTimerRef: ReturnType | null = null; isUnmounting = false; state: ConnectorState = { @@ -126,6 +127,11 @@ export function createConnectorWithoutContext( } componentDidMount() { + if (this.cleanupTimerRef) { + clearTimeout(this.cleanupTimerRef); + this.cleanupTimerRef = null; + } + this.unsubscribe = this.props.contextValue.store.subscribe(() => { if (!this.isUnmounting) { this.setState({ @@ -193,32 +199,34 @@ export function createConnectorWithoutContext( } componentWillUnmount() { - this.isUnmounting = true; + this.cleanupTimerRef = setTimeout(() => { + this.isUnmounting = true; - if (this.unsubscribe) { - this.unsubscribe(); - } + if (this.unsubscribe) { + this.unsubscribe(); + } - if (this.unregisterWidget) { - this.unregisterWidget(); + if (this.unregisterWidget) { + this.unregisterWidget(); - if (typeof connectorDesc.cleanUp === 'function') { - const nextState = connectorDesc.cleanUp.call( - this, - this.props, - this.props.contextValue.store.getState().widgets - ); + if (typeof connectorDesc.cleanUp === 'function') { + const nextState = connectorDesc.cleanUp.call( + this, + this.props, + this.props.contextValue.store.getState().widgets + ); - this.props.contextValue.store.setState({ - ...this.props.contextValue.store.getState(), - widgets: nextState, - }); + this.props.contextValue.store.setState({ + ...this.props.contextValue.store.getState(), + widgets: nextState, + }); - this.props.contextValue.onSearchStateChange( - removeEmptyKey(nextState) - ); + this.props.contextValue.onSearchStateChange( + removeEmptyKey(nextState) + ); + } } - } + }); } getProvidedProps(props) { diff --git a/packages/react-instantsearch-core/src/widgets/InstantSearch.tsx b/packages/react-instantsearch-core/src/widgets/InstantSearch.tsx index 89b93368fb..a4df57c104 100644 --- a/packages/react-instantsearch-core/src/widgets/InstantSearch.tsx +++ b/packages/react-instantsearch-core/src/widgets/InstantSearch.tsx @@ -175,6 +175,7 @@ class InstantSearch extends Component { }; } + cleanupTimerRef: ReturnType | null = null; isUnmounting: boolean = false; constructor(props: Props) { @@ -235,6 +236,11 @@ class InstantSearch extends Component { } componentDidMount() { + if (this.cleanupTimerRef) { + clearTimeout(this.cleanupTimerRef); + this.cleanupTimerRef = null; + } + if (isMetadataEnabled()) { injectMetadata( this.state.instantSearchManager.widgetsManager.getWidgets(), @@ -244,8 +250,10 @@ class InstantSearch extends Component { } componentWillUnmount() { - this.isUnmounting = true; - this.state.instantSearchManager.skipSearch(); + this.cleanupTimerRef = setTimeout(() => { + this.isUnmounting = true; + this.state.instantSearchManager.skipSearch(); + }); } createHrefForState(searchState: SearchState) { diff --git a/packages/react-instantsearch-core/src/widgets/__tests__/InstantSearch.js b/packages/react-instantsearch-core/src/widgets/__tests__/InstantSearch.js index 6cdbd268a4..d5e9b88f41 100644 --- a/packages/react-instantsearch-core/src/widgets/__tests__/InstantSearch.js +++ b/packages/react-instantsearch-core/src/widgets/__tests__/InstantSearch.js @@ -4,6 +4,7 @@ import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import createInstantSearchManager from '../../core/createInstantSearchManager'; import InstantSearch from '../InstantSearch'; import { InstantSearchConsumer } from '../../core/context'; +import { wait } from '../../../../../test/utils'; Enzyme.configure({ adapter: new Adapter() }); @@ -330,7 +331,7 @@ describe('InstantSearch', () => { expect(childContext.widgetsManager).toBe(ism.widgetsManager); }); - it('onSearchStateChange should not be called and search should be skipped if the widget is unmounted', () => { + it('onSearchStateChange should not be called and search should be skipped if the widget is unmounted', async () => { const ism = createFakeInstantSearchManager(); let childContext; createInstantSearchManager.mockImplementation(() => ism); @@ -350,6 +351,9 @@ describe('InstantSearch', () => { ); wrapper.unmount(); + + await wait(0); + childContext.onSearchStateChange({}); expect(onSearchStateChangeMock).toHaveBeenCalledTimes(0);