Skip to content

[combobox] Support primitive item values#4985

Open
atomiks wants to merge 6 commits into
mui:masterfrom
atomiks:claude/elated-dhawan-7d59ce
Open

[combobox] Support primitive item values#4985
atomiks wants to merge 6 commits into
mui:masterfrom
atomiks:claude/elated-dhawan-7d59ce

Conversation

@atomiks

@atomiks atomiks commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Alternative to #4686. Partially addresses #3594. Related to #4419 and #4735.

Adds an itemToValue prop to <Combobox.Root> so objects in items can use a different value for selection:

<Combobox.Root items={countries} itemToValue={(country) => country.code} />

https://deploy-preview-4985--base-ui.netlify.app/react/components/combobox#item-values

itemToValue is needed because the root uses item values before <Combobox.Item> mounts, including for filtering, closed-state typeahead, autofill, and virtualized lists. Reading the value from <Combobox.Item> happens too late, depends on mount timing and object identity, and cannot cover virtualized items that never mount.

The other alternatives do not work here. Requiring items and values to have the same shape removes this use case, standardizing on { value, label } is too restrictive, and putting the getter on a descendant still leaves the root without the value when it needs it.

Filtering and rendering continue to use the source item; selection state, callbacks, and comparators use the projected value. The source item type is inferred separately from the selected value type, and the same behavior applies to virtualized lists.

@atomiks
atomiks force-pushed the claude/elated-dhawan-7d59ce branch from 16898c7 to 914e245 Compare June 9, 2026 03:43
@pkg-pr-new

pkg-pr-new Bot commented Jun 9, 2026

Copy link
Copy Markdown

commit: aba1091

@netlify

netlify Bot commented Jun 9, 2026

Copy link
Copy Markdown

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit 16898c7
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6a278ba88363b1000812c39c
😎 Deploy Preview https://deploy-preview-4985--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@code-infra-dashboard

code-infra-dashboard Bot commented Jun 9, 2026

Copy link
Copy Markdown

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 🔺+1.28KB(+0.29%) 🔺+532B(+0.36%)

Details of bundle changes

Performance

Total duration: 1,074.98 ms -126.62 ms(-10.5%) | Renders: 78 (+0) | Paint: 1,691.22 ms -208.89 ms(-11.0%)

Test Duration Renders
Mixed surface mount (app-like density) 64.51 ms ▼-20.33 ms(-24.0%) 5 (+0)

14 tests within noise — details


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

@netlify

netlify Bot commented Jun 9, 2026

Copy link
Copy Markdown

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit aba1091
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6a5e213d1f80580008f23f10
😎 Deploy Preview https://deploy-preview-4985--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@atomiks atomiks added type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. component: combobox Changes related to the combobox component. labels Jun 9, 2026
@atomiks
atomiks marked this pull request as ready for review June 9, 2026 06:54
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jun 16, 2026
@atomiks
atomiks force-pushed the claude/elated-dhawan-7d59ce branch from 88cfc19 to ce8c0fb Compare June 23, 2026 13:41
@atomiks
atomiks requested a review from jjenzz as a code owner June 23, 2026 13:41
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jun 23, 2026

@flaviendelangle flaviendelangle left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not sure if you added the renderItems param for a future change or if Claude Code is just wrong 😆

PR review

This PR adds primitive-value inference for { value, label } combobox items. The core change — routing label/index/highlight resolution through the new resolveValueLabel helpers — is well-structured, backward-compatible (whole-item and inline-children paths are preserved), and SSR-safe. Nothing is merge-blocking. The one actionable item is dead code left over from an earlier approach; the test matrix is strong and the docs are accurate. Verification was static (I did not run the suite against the working tree to avoid disturbing unrelated uncommitted changes).

Bugs (1)

1. ℹ️ In-place valuesRef mutation assumes virtualized never toggles at runtime

Location: packages/react/src/combobox/root/AriaCombobox.tsx:878-894

if (virtualized) {
  valuesRef.current = flatFilteredItems; // aliases the readonly items/prop array
  ...
  return;
}
// non-virtualized: mutate valuesRef.current in place
const visibleMap = valuesRef.current;
visibleMap.length = flatFilteredItems.length;
for (...) { if (visibleMap[index] === undefined) visibleMap[index] = inferItemValue(...); }

The new non-virtualized branch mutates valuesRef.current in place, relying on the comment's claim that it is "a distinct array." That holds as long as virtualized is stable. If virtualized flips true → false during the component's life, valuesRef.current would still be the readonly items/flatFilteredItems array aliased by the virtualized branch, and the next non-virtualized commit would mutate the user's prop array (visibleMap.length = …, visibleMap[index] = …).

Failure scenario: A consumer that toggles virtualized at runtime gets their items array length/contents mutated in place. This is a narrow, effectively-unsupported pattern (structural prop), so it's a heads-up rather than a blocker — but the safety comment the PR adds (lines 326-331) doesn't cover it.

Fix: If toggling virtualized is not supported, no code change is needed — just narrow the comment to say the distinct-array invariant depends on virtualized being stable. Otherwise, reset valuesRef.current = [] when entering the non-virtualized branch before filling it.

Tests (0)

No findings.

Simplifications (1)

1. 🟡 Dead renderItems parameter on combobox forceMount

Location: packages/react/src/combobox/root/AriaCombobox.tsx:481-489

const forceMount = useStableCallback((renderItems = false) => {
  if (items) {
    labelsRef.current = flatFilteredItems.map((item) => stringifyAsLabel(item, itemToStringLabel));
    if (renderItems && !virtualized) {
      store.set('forceMounted', true);   // never reached
    }
  } else {
    store.set('forceMounted', true);
  }
});

forceMount gained a renderItems parameter and the store type was widened to forceMount: (renderItems?: boolean) => void (store.ts:75), but no call site ever passes true. The three call sites (AriaCombobox.tsx:499, :1413, ComboboxTrigger.tsx:222) call forceMount(), and focusTimeout.start(0, store.state.forceMount) (ComboboxTrigger.tsx:195) invokes it with no arguments. So the if (renderItems && !virtualized) branch is unreachable.

Failure scenario: Unused public-surface (store type) and a dead branch ship as maintenance debt; a future reader has to reverse-engineer whether the closed-list typeahead path is meant to force-mount items (it isn't — valuesRef is filled by the root effect via inferItemValue, so mounting is unnecessary).

Fix: Remove the renderItems parameter and the dead branch, and revert the store.ts type to forceMount: () => void. (If trigger-focus force-mounting was intended, wire focusTimeout.start(0, () => store.state.forceMount(true)) instead — but the current valuesRef fill makes it redundant.)

Docs (0)

No findings. The page.mdx, types.md, and items JSDoc updates accurately describe the new non-virtualized-only inference and the single- vs multiple-selection rendering behavior.

Verdict

Approve after nits — no correctness regressions found; the only real cleanup is the dead renderItems parameter, and the valuesRef mutation note is a narrow heads-up.


🤖 Review generated with Claude Code

@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 10, 2026
@atomiks
atomiks force-pushed the claude/elated-dhawan-7d59ce branch from 7edec3e to a601bc3 Compare July 10, 2026 13:03
@github-actions github-actions Bot added PR: out-of-date The pull request has merge conflicts and can't be merged. and removed PR: out-of-date The pull request has merge conflicts and can't be merged. labels Jul 10, 2026
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 14, 2026
@atomiks
atomiks force-pushed the claude/elated-dhawan-7d59ce branch from d474ed3 to 7263295 Compare July 14, 2026 07:36
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 15, 2026
@atomiks
atomiks force-pushed the claude/elated-dhawan-7d59ce branch from a91b762 to fb444ed Compare July 19, 2026 22:49
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: combobox Changes related to the combobox component. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants