Skip to content

[Autocomplete] Cannot read properties of null (reading 'removeAttribute') in syncHighlightedIndexToDOM under StrictModeΒ #48908

Description

@tsgit18

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:

Metadata

Metadata

Assignees

Labels

scope: autocompleteChanges related to the autocomplete. This includes ComboBox.status: waiting for authorIssue with insufficient information.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions