Skip to content

Commit

Permalink
Merge pull request #116 from adrianchircats/fix-autocomple-render-loo…
Browse files Browse the repository at this point in the history
…p-problem

Autocomplete with loadOptions function enters infinite rerender loop
  • Loading branch information
alexandra-c authored Jul 3, 2024
2 parents 15e37ae + 974777e commit 6c013c3
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 28 deletions.
72 changes: 46 additions & 26 deletions src/components/inputs/Autocomplete/Autocomplete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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()
})
Expand All @@ -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}', () => {
Expand All @@ -467,9 +474,11 @@ describe('Async Autocomplete', () => {
render(
<Autocomplete loadOptions={mockLoadOptions} value={basicOptions[0]} defaultOptions={true} 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('calls loadOptions with input value - when defaultOptions is an array', async () => {
Expand All @@ -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)
})
})

Expand All @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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)
})
})
})
Expand All @@ -606,8 +624,10 @@ describe('Async Multi-value Autocomplete', () => {
const promise = Promise.resolve(basicOptions)
const mockLoadOptions = jest.fn(() => promise)
render(<Autocomplete isMultiSelection simpleValue loadOptions={mockLoadOptions} value={[1]} onChange={jest.fn()} />)
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}', () => {
Expand Down
6 changes: 4 additions & 2 deletions src/components/inputs/Autocomplete/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -334,6 +334,8 @@ const Autocomplete: React.FC<AutocompleteProps<any, any, any, any>> = ({
return simpleValue ? getSimpleValue(loadOptions ? asyncOptions : options, value, valueKey, isMultiSelection) : value
}, [simpleValue, loadOptions, asyncOptions, options, value, valueKey, isMultiSelection])

const debouncedOnInputChange = useDebouncedCallback(handleInputChange, 500)

return (
<MuiAutocomplete
noOptionsText={<NoOptionsText color={typographyContentColor}>{localNoOptionsText}</NoOptionsText>}
Expand All @@ -360,7 +362,7 @@ const Autocomplete: React.FC<AutocompleteProps<any, any, any, any>> = ({
value={localValue}
multiple={isMultiSelection}
onChange={handleChange}
onInputChange={handleInputChange}
onInputChange={debouncedOnInputChange}
disableClearable={!isClearable}
renderOption={renderOption}
renderInput={renderInput}
Expand Down

0 comments on commit 6c013c3

Please sign in to comment.