Skip to content

[autocomplete] Support values other than raw options - #49078

Open
silviuaavram wants to merge 12 commits into
mui:masterfrom
silviuaavram:feat/autocomplete-get-option-value-recovered
Open

[autocomplete] Support values other than raw options#49078
silviuaavram wants to merge 12 commits into
mui:masterfrom
silviuaavram:feat/autocomplete-get-option-value-recovered

Conversation

@silviuaavram

@silviuaavram silviuaavram commented Sep 1, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #23708 by adding a getOptionValue prop to Autocomplete and useAutocomplete.

getOptionValue lets consumers store a primitive option identifier in value, defaultValue, and onChange instead of storing the complete option object.

const options = [
  { id: 'godfather', label: 'The Godfather' },
  { id: 'pulp-fiction', label: 'Pulp Fiction' },
];

<Autocomplete
  options={options}
  getOptionValue={(option) => option.id}
  value="godfather"
  onChange={(event, value) => {
    // value is the selected option ID
  }}
  getOptionLabel={(option) => option.label}
  renderInput={(params) => <TextField {...params} />}
/>;

Without getOptionValue, the existing object-value behavior remains unchanged.

API behavior

When getOptionValue is provided:

  • value and defaultValue contain the value returned by getOptionValue.
  • onChange receives the mapped value.
  • Multiple selection uses an array of mapped values.
  • renderValue receives the mapped value.
  • isOptionEqualToValue receives the original option as its first argument and the mapped value as its second argument.
  • Option-facing callbacks such as getOptionLabel, getOptionDisabled, getOptionKey, and renderOption continue to receive the original option.
  • onChange details continue to expose the original option when one is available.

The returned option value must be a unique, non-null primitive: string, number, bigint, or boolean.

Development-time validation reports invalid and duplicate option values.

Internal flow

Options are converted to their external values only when an option selection is committed:

getOptionValue(option);

The hook memoizes the inverse lookup needed to resolve controlled mapped values back to their original options.

This resolution is used before calling option-facing APIs, including when:

  • Initializing or resetting the input label.
  • Checking whether the input displays the selected value.
  • Preserving the highlighted option when options change.
  • Rendering default chips.
  • Producing removal details.

When isOptionEqualToValue is provided, it defines how a mapped value is resolved to an option. Otherwise, a memoized map provides constant-time lookup.

useAutocomplete also exposes getOptionFromValue so wrappers such as Autocomplete can perform the same resolution without duplicating the map or matching logic.

freeSolo values

Values created from free-solo input bypass getOptionValue.

For example, with a numeric option mapping:

<Autocomplete
  freeSolo
  options={[
    { id: 1, label: 'Draft' },
    { id: 2, label: 'Published' },
  ]}
  getOptionValue={(option) => option.id}
/>

Selecting an option produces 1 or 2, while entering custom text produces the entered string.

This preserves the existing contract that free-solo values are strings and prevents free-solo text from being passed to an option-only callback.

String collisions in freeSolo

A mapped string value and a free-solo string cannot be distinguished after they enter the controlled value API.

For example:

const options = [{ id: 'draft', label: 'Published' }];

<Autocomplete
  freeSolo
  options={options}
  getOptionValue={(option) => option.id}
  value="draft"
/>

The value "draft" could mean either:

  • The option whose mapped ID is "draft".
  • The free-solo text entered by the user.

Previously, mapped option lookup took precedence. As a result, committing the free-solo text "draft" could immediately resolve to the Published option, replace the input label, render the wrong chip label, and mark that option as selected.

Because controlled values do not retain their origin, this ambiguity cannot be resolved reliably from the string alone.

The API therefore uses the following explicit policy:

  • Strings are reserved for free-solo values whenever freeSolo and getOptionValue are used together.
  • getOptionValue must return a number, bigint, or boolean in freeSolo mode.
  • TypeScript rejects a string-returning getOptionValue when freeSolo can be enabled.
  • Development builds report a descriptive error for string mappings.
  • For unsupported JavaScript configurations, runtime resolution gives free-solo strings precedence over mapped options.
  • Equality checks and the selected-value fast path do not match free-solo strings against options.
  • Custom isOptionEqualToValue callbacks cannot override the free-solo string distinction.

String mappings remain supported when freeSolo is disabled.

TypeScript

The mapped value type is inferred from getOptionValue:

<Autocomplete
  options={options}
  getOptionValue={(option) => option.id}
  onChange={(event, value) => {
    // value: string | null
  }}
/>

Multiple selection produces an array:

<Autocomplete
  multiple
  options={options}
  getOptionValue={(option) => option.id}
  onChange={(event, value) => {
    // value: string[]
  }}
/>

In freeSolo, the mapped value must be non-string so that the resulting union remains distinguishable:

<Autocomplete
  freeSolo
  options={options}
  getOptionValue={(option) => Number(option.id)}
  onChange={(event, value) => {
    // value: number | string | null
  }}
/>

Tests

Added coverage for:

  • Default mapped-value equality.
  • Single and multiple selection.
  • Controlled and default mapped values.
  • Input and chip labels.
  • Custom equality.
  • Filtering selected options.
  • Toggling and removing mapped values.
  • Unmatched mapped values.
  • Highlight preservation when options change.
  • Option-facing callback arguments.
  • Free-solo values bypassing getOptionValue.
  • Supported non-string mappings in freeSolo.
  • Free-solo text colliding with a mapped string value.
  • Controlled collision selection state.
  • Default chip rendering for colliding values.
  • Collision handling with custom equality.
  • Invalid and duplicate option-value validation.
  • Type inference and invalid TypeScript combinations.

Documentation and generated API descriptions were also updated with the mapped-value and freeSolo constraints.

@silviuaavram silviuaavram added type: new feature Expand the scope of the product to solve a new problem. scope: autocomplete Changes related to the autocomplete. This includes ComboBox. labels Sep 1, 2026
@code-infra-dashboard

code-infra-dashboard Bot commented Sep 1, 2026

Copy link
Copy Markdown

Deploy preview

Bundle size

Bundle Parsed size Gzip size
@mui/material 🔺+714B(+0.13%) 🔺+303B(+0.20%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@silviuaavram silviuaavram changed the title Feat/autocomplete get option value recovered [autocomplete] Support values other than raw options Sep 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical mapped-label resolution defects and moderate prop-forwarding and validation issues remain unresolved.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds primitive mapped values to Autocomplete and useAutocomplete through getOptionValue.

Changes:

  • Extends runtime behavior and TypeScript inference for mapped values.
  • Adds validation, equality handling, and regression/type tests.
  • Preserves existing option-object behavior when mapping is omitted.
File summaries
File Review
packages/mui-material/src/useAutocomplete/utils/validateOptionValues.ts Validation can crash for repeated invalid mapper outputs.
packages/mui-material/src/useAutocomplete/useAutocomplete.test.js Adds runtime coverage for mapping, equality, and validation.
packages/mui-material/src/useAutocomplete/useAutocomplete.spec.ts Adds hook type coverage.
packages/mui-material/src/useAutocomplete/useAutocomplete.js Mapped values are not consistently resolved before label callbacks.
packages/mui-material/src/useAutocomplete/useAutocomplete.d.ts Documented mapped-value flows still pass values directly to option-facing callbacks.
packages/mui-material/src/Autocomplete/Autocomplete.spec.tsx Adds component type coverage.
packages/mui-material/src/Autocomplete/Autocomplete.d.ts Declared prop is forwarded to the DOM instead of being consumed by the component.
Review details

Suppressed comments (2)

packages/mui-material/src/useAutocomplete/useAutocomplete.js:144

  • getOptionValue is only applied during equality checks. selectNewValue still assigns/pushes the raw option (lines 808 and 829), so selecting an unselected option makes onChange and internal state contain the option object rather than the declared mapped primitive (for example, ['foo', options[1]] instead of ['foo', 'bar']). Map option-origin selections before calling handleValue, while leaving free-solo strings unchanged.
      return getOptionValue(option) === value2;

packages/mui-material/src/useAutocomplete/useAutocomplete.js:144

  • In multiple free-solo mode, selectNewValue calls this comparator with the newly typed string as the option argument while checking existing values. That forwards a free-solo string to getOptionValue (or to the custom comparator's Option parameter), contrary to the new callback contract and can throw for object-specific mappers. Handle free-solo duplicate/toggle logic without invoking option-only callbacks.
      if (isOptionEqualToValueProp) {
        return isOptionEqualToValueProp(option, value2);
      }

      return getOptionValue(option) === value2;
  • Files reviewed: 5/7 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/mui-material/src/useAutocomplete/useAutocomplete.js

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical free-solo collision and mapped-value typing issues remain unresolved.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 14/16 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread packages/mui-material/src/useAutocomplete/useAutocomplete.js Outdated
@silviuaavram silviuaavram self-assigned this Sep 4, 2026
@silviuaavram
silviuaavram marked this pull request as ready for review September 4, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: autocomplete Changes related to the autocomplete. This includes ComboBox. type: new feature Expand the scope of the product to solve a new problem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[autocomplete] Support values other than raw options

2 participants