[composite] Support controlled item indexes#5148
Conversation
commit: |
Bundle size
PerformanceTotal duration: 1,147.81 ms ▼-320.41 ms(-21.8%) | Renders: 76 (▼-2)
…and 2 more (+7 within noise) — details Check out the code infra dashboard for more information about this PR. |
✅ Deploy Preview for base-ui ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
62d636f to
26171d8
Compare
| export type CompositeMetadata<CustomMetadata> = { | ||
| index?: number | null | undefined; | ||
| } & CustomMetadata; |
There was a problem hiding this comment.
moved this here to avoid circular imports
195aff9 to
32f4977
Compare
c0ebe9f to
b6f1165
Compare
8bb53ad to
5b2fdbf
Compare
5b2fdbf to
6936144
Compare
| listeners.add(fn); | ||
| // Delayed items need the latest snapshot when they subscribe, otherwise | ||
| // they can stay on a guessed index until another list change happens. | ||
| fn(sortedMapRef.current); |
There was a problem hiding this comment.
The OTPField highlighted an edge case where an item could keep its guessed index because items subscribe to map changes after registering themselves, and this PR now produces the sorted map during registering (before the subscription).
AFAIUI, this wasn't a problem before because map listeners were notified after the mapTick state update, from within a layout effect tied to that state change, so index correction happened later in the render/effect cycle. The old callback ref writes were covering up that correction delay.
Now that the sorted map and refs are produced during registration,elementsRef/labelsRef are ready without waiting for that state/effect cycle. That means listener notification also happens earlier, so if a child subscribes after registering it wouldn't get the correction snapshot.
Long story short, we probably want to make sure we subscribe before we register, but this fix seems more correct/robust.
734dbf7 to
2d8d998
Compare
2d8d998 to
20d103b
Compare
This comment was marked as outdated.
This comment was marked as outdated.
60e1af5 to
36f6a40
Compare
275c005 to
f5a1dd1
Compare
Performance: measured, one real inefficiency foundBenchmarked
† master's controlled numbers are the skip-registration path — fast because it did nothing, which is exactly the bug being fixed. Reading the middle column: the PR is perf-neutral for uncontrolled lists (the common case), but large controlled lists pay real new cost — a filter keystroke on a 1000-item indexed Combobox costs ~34 ms more than master, per keystroke. The cost is not the sort — it's the state-update enqueue. Both map.set(node, metadata);
if (!isDirtyRef.current) {
isDirtyRef.current = true;
setMapTick((tick) => tick + 1);
}(and the same in Minimality assessmentThe architecture is close to minimal for what it has to preserve — I looked for structural cuts and found none that survive scrutiny: the
The single actionable outcome of this pass is the 🤖 Review generated with Claude Code |
Second pass: virtualized regressionsFollow-up hunt on the current head — the earlier findings (deep-subtree registration, per-register tick cost, 1. 🔴 Virtualized:
|
27c41a1 to
76da89c
Compare
76da89c to
d028a8d
Compare
d028a8d to
d5f6916
Compare
Fixes #4657
This updates
useCompositeListItemso passing an explicitindexmakes the item index controlled without opting the item out ofCompositeListregistration.Previously, items with an explicit
indexskipped registration entirely. In non-virtualized Combobox/Autocomplete lists,useListNavigationstill relies onlistRefto resolve pointer hover targets, solistRef.current.indexOf(event.currentTarget)could not find hovered items anddata-highlightedstayed on the previous item.The new behavior:
indexitems still register withCompositeListCompositeListreport a development errorAdded test coverage for:
@atomiks it looks like you've touched this file the most, so would really appreciate your eyes on this one 🙏 lmk if this could have been achieved more simply elsewhere.
CleanShot.2026-06-30.at.16.50.53.mp4
CleanShot.2026-06-30.at.16.53.00.mp4