Duplicates
Steps to reproduce πΉ
Observed in a real app (not yet isolated to a minimal sandbox β happy to build one if useful): an Autocomplete with a custom renderOption, rendered inside <React.StrictMode>, inside a dialog. Interacting with the dropdown (opening it / moving the highlighted option) intermittently throws on mount. The timing lines up with StrictMode's dev-only double-invocation of effects (mount β cleanup β mount), which is what leaves inputRef.current and listboxRef.current in an inconsistent state relative to each other rather than both clearing together.
Current behavior π―
Intermittent uncaught crash:
TypeError: Cannot read properties of null (reading 'removeAttribute')
at syncHighlightedIndexToDOM (useAutocomplete.js)
In packages/@mui/material/useAutocomplete/useAutocomplete.js, syncHighlightedIndexToDOM guards against both inputRef.current and listboxRef.current being null, then unconditionally dereferences inputRef.current:
// React can clear refs before pending passive effects run during unmount.
// If both refs are gone, there is no DOM left to sync.
if (inputRef.current == null && listboxRef.current == null) {
return;
}
// does the index exist?
if (index === -1) {
inputRef.current.removeAttribute('aria-activedescendant');
} else {
inputRef.current.setAttribute('aria-activedescendant', `${id}-option-${index}`);
}
If inputRef.current is null while listboxRef.current is still set (the two refs don't necessarily clear in the same commit β the comment directly above even acknowledges refs can clear asymmetrically during unmount), the && guard passes through and the next line throws. This is consistent with the "each ref must be checked independently" root cause discussed on #25273, just resurfaced against the current syncHighlightedIndexToDOM implementation (a later refactor β no existing issue references this function by name).
I recognize #25273 was closed in 2021 with the position that "supporting out-of-order [ref/effect resolution]... doesn't seem worth the extra complexity" for unusual custom-Popper compositions. This report is narrower: it reproduces under plain <Autocomplete> (no custom Popper) purely from React StrictMode's double-invocation of effects, which React's own docs require components to tolerate. So this isn't "user-land assembled it unusually" β it's the library's cleanup path not surviving the React-recommended dev-mode double-invoke contract.
Expected behavior π€
syncHighlightedIndexToDOM should guard the inputRef dereference independently of listboxRef, e.g.:
if (inputRef.current != null) {
if (index === -1) {
inputRef.current.removeAttribute('aria-activedescendant');
} else {
inputRef.current.setAttribute('aria-activedescendant', `${id}-option-${index}`);
}
}
if (inputRef.current == null && listboxRef.current == null) {
return;
}
Happy to open a PR with this + a regression test (e.g. mounting under StrictMode and asserting no console error) if that framing is welcome β wanted to check first given the history on #25273.
Context π¦
Found while building a staff-assignment Autocomplete with a custom renderOption. Confirmed via source inspection that the exact null-guard bug is present in the currently-published 9.2.0. Patched locally with patch-package in the meantime.
Your environment π
@mui/material: 9.2.0
- React: 19.2.8, rendered inside
<React.StrictMode>
- Bundler: Vite (dev server)
- Reproduces in dev (
npm run dev), consistent with StrictMode's dev-only double-invocation of effects.
Search keywords:
Duplicates
Steps to reproduce πΉ
Observed in a real app (not yet isolated to a minimal sandbox β happy to build one if useful): an
Autocompletewith a customrenderOption, rendered inside<React.StrictMode>, inside a dialog. Interacting with the dropdown (opening it / moving the highlighted option) intermittently throws on mount. The timing lines up with StrictMode's dev-only double-invocation of effects (mount β cleanup β mount), which is what leavesinputRef.currentandlistboxRef.currentin an inconsistent state relative to each other rather than both clearing together.Current behavior π―
Intermittent uncaught crash:
In
packages/@mui/material/useAutocomplete/useAutocomplete.js,syncHighlightedIndexToDOMguards against bothinputRef.currentandlistboxRef.currentbeingnull, then unconditionally dereferencesinputRef.current:If
inputRef.currentisnullwhilelistboxRef.currentis still set (the two refs don't necessarily clear in the same commit β the comment directly above even acknowledges refs can clear asymmetrically during unmount), the&&guard passes through and the next line throws. This is consistent with the "each ref must be checked independently" root cause discussed on #25273, just resurfaced against the currentsyncHighlightedIndexToDOMimplementation (a later refactor β no existing issue references this function by name).I recognize #25273 was closed in 2021 with the position that "supporting out-of-order [ref/effect resolution]... doesn't seem worth the extra complexity" for unusual custom-Popper compositions. This report is narrower: it reproduces under plain
<Autocomplete>(no custom Popper) purely from React StrictMode's double-invocation of effects, which React's own docs require components to tolerate. So this isn't "user-land assembled it unusually" β it's the library's cleanup path not surviving the React-recommended dev-mode double-invoke contract.Expected behavior π€
syncHighlightedIndexToDOMshould guard theinputRefdereference independently oflistboxRef, e.g.:Happy to open a PR with this + a regression test (e.g. mounting under
StrictModeand asserting no console error) if that framing is welcome β wanted to check first given the history on #25273.Context π¦
Found while building a staff-assignment
Autocompletewith a customrenderOption. Confirmed via source inspection that the exact null-guard bug is present in the currently-published 9.2.0. Patched locally withpatch-packagein the meantime.Your environment π
@mui/material: 9.2.0<React.StrictMode>npm run dev), consistent with StrictMode's dev-only double-invocation of effects.Search keywords: