I have a dynamic search modal that queries the backend as users type. When a user selects one of the options it saves that option in state and is rendered as a Chip
component. However, after updating our dependencies, the modal will not launch and the console is displaying this message
“Warning: Maximum update depth exceeded. This can happen when a
component calls setState inside useEffect, but useEffect either
doesn’t have a dependency array, or one of the dependencies changes on
every render.”
I logged the dependencies of my useEffect and none of them appear to be changing from render to render, however they are logged more times than I’d expect. Here is the component:
const SearchModalReturnField = ({
open,
fetchService,
fetchQueryFieldName,
handleClose,
value,
setValue,
...props
}) => {
const [modalValue, setModalValue] = useState([]);
const [inputValue, setInputValue] = useState('');
const [options, setOptions] = useState([]);
const [error, setError] = useState();
const handleChangeInput = (event) => {
setInputValue(event.target.value);
};
const handleChange = (event, newValue) => {
setModalValue(uniqWith(newValue, isEqual));
};
const handleSelectIngredient = (event, newValue) => {
setInputValue('');
if (options.length > 0) {
setModalValue(uniqWith([...modalValue, newValue], isEqual));
}
};
const fetch = useMemo(
() =>
debounce((params, callback) => {
fetchService(params)
.then(callback)
.catch((error) => {
console.error(error);
if (error && error.message) {
return setError('Error: ' + error.message);
}
if (typeof error === 'string') {
return setError('Error: ' + error);
}
return setError('Error encountered');
});
}, 500),
[fetchService],
);
useEffect(() => {
let active = true;
if (inputValue === '') {
setOptions([]);
return undefined;
}
if (fetchService) {
const params = {
query: inputValue,
limit: 200,
};
fetch(params, (results) => {
if (active) {
setOptions(map(results, fetchQueryFieldName) || []);
}
});
}
return () => {
active = false;
};
}, [inputValue, fetchQueryFieldName]);
const handleDelete = (item) => {
setModalValue(filter(modalValue, (v) => v !== item));
};
const handleAccept = () => {
setValue(uniqWith(concat(value, modalValue), isEqual));
setModalValue([]);
handleClose();
};
const handleCloseModal = () => {
setModalValue([]);
handleClose();
};
const optionsList = options.map((option, index) => {
return (
<OptionLabel
key={index}
onClick={(e) => handleSelectIngredient(e, option)}
data-test="searchModal-foodOption"
>
{getHighlightedText(capitalize(option), inputValue)}
</OptionLabel>
);
});
if (isMobile()) {
// Mobile view
return (
<MobileDialog
open={open}
handleClose={handleClose}
height="fit-content"
maxheight="85%"
small="small"
data-test="mobile-modal"
>
<DialogContainer flexDirection="column" alignItems="center">
<Puller />
<TopContainer flexDirection="column" alignItems="center">
<MobileHeading>{props.heading}</MobileHeading>
<MobileSubheading marginTop={'8px'}>
{props.subheading}
</MobileSubheading>
</TopContainer>
<SearchContainer flexDirection="column" alignSelf="flex-start">
<MobileInputLabel>Search for food</MobileInputLabel>
<DynamicSearchBar
searchTerm={inputValue}
handleChange={handleChangeInput}
data-test="searchModal-ingSearch"
/>
</SearchContainer>
<MobileChipContainer wrap="wrap" alignItems="center">
{modalValue.map((option, index) => (
<ChipStyled
key={index}
label={props.getOptionLabel(option)}
handleDelete={() => handleDelete(option)}
bgcolor={props.chipBgColor}
iconcolor="#FFFBFB"
selected
data-test="searchModal-ingChip"
/>
))}
</MobileChipContainer>
<OptionsContainer flexDirection="column">
{optionsList}
</OptionsContainer>
{error && (
<AlertStack
messages={error}
type="error"
variant="filled"
open={!!error}
handleClose={() => setError(null)}
autoHideDuration={10000}
/>
)}
<MobileAddButton
alignItems="center"
justify="center"
backgroundcolor={props.chipBgColor}
onClick={handleAccept}
data-test="submit-items"
>
<MobileSubheading color="#fff">
Add {props.heading}
</MobileSubheading>
</MobileAddButton>
<div style={{ height: 8 }} />
<MobileAddButton
alignItems="center"
justify="center"
backgroundcolor="#fff"
borderColor={colors.primary800}
onClick={handleCloseModal}
>
<MobileSubheading color={colors.primary800}>
Cancel
</MobileSubheading>
</MobileAddButton>
</DialogContainer>
</MobileDialog>
);
} else {
// Web View
return (
<Dialog onClose={handleClose} open={open}>
<DialogTitle>
{handleClose ? (
<TitleButton aria-label="close" onClick={handleCloseModal}>
<CloseIcon />
</TitleButton>
) : null}
</DialogTitle>
<DialogContent>
<Title>Search Foods</Title>
<AutocompleteStyled
multiple
autoComplete
freeSolo
includeInputInList
filterOptions={(x) => x}
options={options}
getOptionLabel={props.getOptionLabel || ((option) => option.name)}
value={modalValue}
onChange={handleChange}
renderInput={(params) => (
<InputBase
ref={params.InputProps.ref}
inputProps={{
...params.inputProps,
onChange(event) {
handleChangeInput(event);
return params.inputProps.onChange(event);
},
}}
placeholder={props.placeholder || ''}
fullWidth
endAdornment={
<InputAdornment position="end">
<IconButton aria-label="search" edge="end">
<SearchIcon />
</IconButton>
</InputAdornment>
}
data-test="searchModal-ingSearch"
/>
)}
/>
{error && (
<AlertStack
messages={error}
type="error"
variant="filled"
open={!!error}
handleClose={() => setError(null)}
autoHideDuration={10000}
/>
)}
{modalValue.map((option, index) => (
<ChipStyled
key={index}
label={props.getOptionLabel(option)}
handleDelete={() => handleDelete(option)}
bgcolor={props.chipBgColor}
hovercolor={props.hoverColor}
iconcolor="#FFFBFB"
data-test="searchModal-ingChip"
selected
/>
))}
</DialogContent>
<DialogActions>
<AddButton
buttonText="ADD"
onClick={handleAccept}
backgroundcolor={props.chipBgColor}
hovercolor={props.hoverColor}
data-test="searchModal-add"
/>
</DialogActions>
</Dialog>
);
}
};