Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autocomplete with loadOptions function enters infinite rerender loop #116

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading