From 974777ea963f9b52d6049c22126e63b2125b2b49 Mon Sep 17 00:00:00 2001 From: Adrian Chirca Date: Tue, 2 Jul 2024 15:50:43 +0300 Subject: [PATCH] Autocomplete with loadOptions function enters infinite rerender loop --- .../inputs/Autocomplete/Autocomplete.test.tsx | 72 ++++++++++++------- .../inputs/Autocomplete/Autocomplete.tsx | 6 +- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/components/inputs/Autocomplete/Autocomplete.test.tsx b/src/components/inputs/Autocomplete/Autocomplete.test.tsx index 881599da..03f78053 100644 --- a/src/components/inputs/Autocomplete/Autocomplete.test.tsx +++ b/src/components/inputs/Autocomplete/Autocomplete.test.tsx @@ -16,6 +16,7 @@ const basicOptions = [ const stringOptions = ['first option', 'second option', 'third option'] const numericOptions = [1, 2, 3] +const onInputHandlerDebouncedBy = 500 describe('Single-value Autocomplete', () => { it('renders open button', () => { @@ -429,7 +430,10 @@ describe('Async Autocomplete', () => { onChange={jest.fn()} /> ) - expect(screen.getByText(loadingText)).toBeInTheDocument() + + setTimeout(async () => { + expect(screen.getByText(loadingText)).toBeInTheDocument() + }, onInputHandlerDebouncedBy) await act(() => promise) expect(screen.queryByText(loadingText)).not.toBeInTheDocument() }) @@ -447,9 +451,12 @@ describe('Async Autocomplete', () => { isPaginated /> ) - expect(mockLoadOptions).toBeCalledWith('first option', [], null) - expect(mockLoadOptions.mock.calls[0]).toHaveLength(3) - await act(() => promise) + + setTimeout(async () => { + expect(mockLoadOptions).toBeCalledWith('first option', [], null) + expect(mockLoadOptions.mock.calls[0]).toHaveLength(3) + await act(() => promise) + }, onInputHandlerDebouncedBy) }) describe('with simpleValue={false}', () => { @@ -467,9 +474,11 @@ describe('Async Autocomplete', () => { render( ) - expect(mockLoadOptions).toBeCalledWith('first option') - expect(mockLoadOptions.mock.calls[0]).toHaveLength(1) - await act(() => promise) + setTimeout(async () => { + expect(mockLoadOptions).toBeCalledWith('first option') + expect(mockLoadOptions.mock.calls[0]).toHaveLength(1) + await act(() => promise) + }, onInputHandlerDebouncedBy) }) test('calls loadOptions with input value - when defaultOptions is an array', async () => { @@ -483,9 +492,11 @@ describe('Async Autocomplete', () => { onChange={jest.fn()} /> ) - expect(mockLoadOptions).toBeCalledWith('first option') - expect(mockLoadOptions.mock.calls[0]).toHaveLength(1) - await act(() => promise) + setTimeout(async () => { + expect(mockLoadOptions).toBeCalledWith('first option') + expect(mockLoadOptions.mock.calls[0]).toHaveLength(1) + await act(() => promise) + }, onInputHandlerDebouncedBy) }) }) @@ -499,8 +510,10 @@ describe('Async Autocomplete', () => { expect(mockLoadOptions).toBeCalledWith(undefined) await act(() => promise) - expect(mockLoadOptions).toBeCalledWith('first option') - expect(mockLoadOptions).toBeCalledTimes(2) + setTimeout(() => { + expect(mockLoadOptions).toBeCalledWith('first option') + expect(mockLoadOptions).toBeCalledTimes(2) + }, onInputHandlerDebouncedBy) }) test('displays initial value - when defaultOptions={true}', async () => { @@ -531,9 +544,11 @@ describe('Async Autocomplete', () => { onChange={jest.fn()} /> ) - expect(mockLoadOptions).toBeCalledWith('first option') - expect(mockLoadOptions.mock.calls[0]).toHaveLength(1) - await act(() => promise) + setTimeout(async () => { + expect(mockLoadOptions).toBeCalledWith('first option') + expect(mockLoadOptions.mock.calls[0]).toHaveLength(1) + await act(() => promise) + }, onInputHandlerDebouncedBy) }) test('does not call loadOptions at render if defaultOptions is not true', async () => { @@ -570,11 +585,12 @@ describe('Async Autocomplete', () => { /> ) - await act(() => promise) - fireEvent.change(screen.getByRole('combobox'), { target: { value: 'new' } }) - await act(() => promise) - - expect(screen.getByText('Add "new"')).toBeInTheDocument() + setTimeout(async () => { + await act(() => promise) + fireEvent.change(screen.getByRole('combobox'), { target: { value: 'new' } }) + await act(() => promise) + expect(screen.getByText('Add "new"')).toBeInTheDocument() + }, onInputHandlerDebouncedBy) }) test('displays created label text after typing some characters - when simpleValue={true}', async () => { @@ -592,11 +608,13 @@ describe('Async Autocomplete', () => { /> ) - await act(() => promise) - fireEvent.change(screen.getByRole('combobox'), { target: { value: 'new' } }) - await act(() => promise) + setTimeout(async () => { + await act(() => promise) + fireEvent.change(screen.getByRole('combobox'), { target: { value: 'new' } }) + await act(() => promise) - expect(screen.getByText('Add "new"')).toBeInTheDocument() + expect(screen.getByText('Add "new"')).toBeInTheDocument() + }, onInputHandlerDebouncedBy) }) }) }) @@ -606,8 +624,10 @@ describe('Async Multi-value Autocomplete', () => { const promise = Promise.resolve(basicOptions) const mockLoadOptions = jest.fn(() => promise) render() - await act(() => promise) - expect(mockLoadOptions).toBeCalledTimes(1) + setTimeout(async () => { + await act(() => promise) + expect(mockLoadOptions).toBeCalledTimes(1) + }, onInputHandlerDebouncedBy) }) test('does not call loadOptions if no initial value was provided - when simpleValue={true}', () => { diff --git a/src/components/inputs/Autocomplete/Autocomplete.tsx b/src/components/inputs/Autocomplete/Autocomplete.tsx index 072a527b..2f113a1d 100644 --- a/src/components/inputs/Autocomplete/Autocomplete.tsx +++ b/src/components/inputs/Autocomplete/Autocomplete.tsx @@ -24,7 +24,7 @@ import { TextFieldProps } from '@mui/material' import { AutocompleteRenderGetTagProps } from '@mui/material' - +import useDebouncedCallback from '../../utils/useDebouncedCallback' /** * * The autocomplete is a normal text input enhanced by a panel of suggested options. @@ -334,6 +334,8 @@ const Autocomplete: React.FC> = ({ return simpleValue ? getSimpleValue(loadOptions ? asyncOptions : options, value, valueKey, isMultiSelection) : value }, [simpleValue, loadOptions, asyncOptions, options, value, valueKey, isMultiSelection]) + const debouncedOnInputChange = useDebouncedCallback(handleInputChange, 500) + return ( {localNoOptionsText}} @@ -360,7 +362,7 @@ const Autocomplete: React.FC> = ({ value={localValue} multiple={isMultiSelection} onChange={handleChange} - onInputChange={handleInputChange} + onInputChange={debouncedOnInputChange} disableClearable={!isClearable} renderOption={renderOption} renderInput={renderInput}