Skip to content

Commit 51775d2

Browse files
authored
Improve performance of Listbox and Menu when closing (#3690)
In a previous PR, we already batched registering options. This PR also batches unregistering options to make the closing behavior smoother when there are a lot of items rendered.
1 parent 51acc1b commit 51775d2

File tree

4 files changed

+56
-16
lines changed

4 files changed

+56
-16
lines changed

packages/@headlessui-react/src/components/listbox/listbox-machine.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export enum ActionTypes {
7272
ClearSearch,
7373

7474
RegisterOptions,
75-
UnregisterOption,
75+
UnregisterOptions,
7676

7777
SetButtonElement,
7878
SetOptionsElement,
@@ -124,7 +124,7 @@ type Actions<T> =
124124
type: ActionTypes.RegisterOptions
125125
options: { id: string; dataRef: ListboxOptionDataRef<T> }[]
126126
}
127-
| { type: ActionTypes.UnregisterOption; id: string }
127+
| { type: ActionTypes.UnregisterOptions; options: string[] }
128128
| { type: ActionTypes.SetButtonElement; element: HTMLButtonElement | null }
129129
| { type: ActionTypes.SetOptionsElement; element: HTMLElement | null }
130130
| { type: ActionTypes.SortOptions }
@@ -328,12 +328,24 @@ let reducers: {
328328
pendingShouldSort: true,
329329
}
330330
},
331-
[ActionTypes.UnregisterOption]: (state, action) => {
331+
[ActionTypes.UnregisterOptions]: (state, action) => {
332332
let options = state.options
333-
let idx = options.findIndex((a) => a.id === action.id)
334-
if (idx !== -1) {
333+
334+
let idxs = []
335+
let ids = new Set(action.options)
336+
for (let [idx, option] of options.entries()) {
337+
if (ids.has(option.id)) {
338+
idxs.push(idx)
339+
ids.delete(option.id)
340+
if (ids.size === 0) break
341+
}
342+
}
343+
344+
if (idxs.length > 0) {
335345
options = options.slice()
336-
options.splice(idx, 1)
346+
for (let idx of idxs.reverse()) {
347+
options.splice(idx, 1)
348+
}
337349
}
338350

339351
return {
@@ -421,6 +433,15 @@ export class ListboxMachine<T> extends Machine<State<T>, Actions<T>> {
421433
},
422434
]
423435
}),
436+
unregisterOption: batch(() => {
437+
let options: string[] = []
438+
return [
439+
(id: string) => options.push(id),
440+
() => {
441+
this.send({ type: ActionTypes.UnregisterOptions, options: options.splice(0) })
442+
},
443+
]
444+
}),
424445
goToOption: batch(() => {
425446
let last: Extract<Actions<unknown>, { type: ActionTypes.GoToOption }> | null = null
426447
return [

packages/@headlessui-react/src/components/listbox/listbox.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -850,9 +850,7 @@ function OptionFn<
850850
useIsoMorphicEffect(() => {
851851
if (usedInSelectedOption) return
852852
machine.actions.registerOption(id, bag)
853-
return () => {
854-
machine.send({ type: ActionTypes.UnregisterOption, id })
855-
}
853+
return () => machine.actions.unregisterOption(id)
856854
}, [bag, id, usedInSelectedOption])
857855

858856
let handleClick = useEvent((event: { preventDefault: Function }) => {

packages/@headlessui-react/src/components/menu/menu-machine.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export enum ActionTypes {
4545
Search,
4646
ClearSearch,
4747
RegisterItems,
48-
UnregisterItem,
48+
UnregisterItems,
4949

5050
SetButtonElement,
5151
SetItemsElement,
@@ -95,7 +95,7 @@ export type Actions =
9595
| { type: ActionTypes.Search; value: string }
9696
| { type: ActionTypes.ClearSearch }
9797
| { type: ActionTypes.RegisterItems; items: { id: string; dataRef: MenuItemDataRef }[] }
98-
| { type: ActionTypes.UnregisterItem; id: string }
98+
| { type: ActionTypes.UnregisterItems; items: string[] }
9999
| { type: ActionTypes.SetButtonElement; element: HTMLButtonElement | null }
100100
| { type: ActionTypes.SetItemsElement; element: HTMLElement | null }
101101
| { type: ActionTypes.SortItems }
@@ -283,12 +283,24 @@ let reducers: {
283283
pendingShouldSort: true,
284284
}
285285
},
286-
[ActionTypes.UnregisterItem]: (state, action) => {
286+
[ActionTypes.UnregisterItems]: (state, action) => {
287287
let items = state.items
288-
let idx = items.findIndex((a) => a.id === action.id)
289-
if (idx !== -1) {
288+
289+
let idxs = []
290+
let ids = new Set(action.items)
291+
for (let [idx, item] of items.entries()) {
292+
if (ids.has(item.id)) {
293+
idxs.push(idx)
294+
ids.delete(item.id)
295+
if (ids.size === 0) break
296+
}
297+
}
298+
299+
if (idxs.length > 0) {
290300
items = items.slice()
291-
items.splice(idx, 1)
301+
for (let idx of idxs.reverse()) {
302+
items.splice(idx, 1)
303+
}
292304
}
293305

294306
return {
@@ -297,6 +309,7 @@ let reducers: {
297309
activationTrigger: ActivationTrigger.Other,
298310
}
299311
},
312+
300313
[ActionTypes.SetButtonElement]: (state, action) => {
301314
if (state.buttonElement === action.element) return state
302315
return { ...state, buttonElement: action.element }
@@ -359,6 +372,14 @@ export class MenuMachine extends Machine<State, Actions> {
359372
() => this.send({ type: ActionTypes.RegisterItems, items: items.splice(0) }),
360373
]
361374
}),
375+
unregisterItem: batch(() => {
376+
let items: string[] = []
377+
378+
return [
379+
(id: string) => items.push(id),
380+
() => this.send({ type: ActionTypes.UnregisterItems, items: items.splice(0) }),
381+
]
382+
}),
362383
}
363384

364385
selectors = {

packages/@headlessui-react/src/components/menu/menu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ function ItemFn<TTag extends ElementType = typeof DEFAULT_ITEM_TAG>(
620620

621621
useIsoMorphicEffect(() => {
622622
machine.actions.registerItem(id, bag)
623-
return () => machine.send({ type: ActionTypes.UnregisterItem, id })
623+
return () => machine.actions.unregisterItem(id)
624624
}, [bag, id])
625625

626626
let close = useEvent(() => {

0 commit comments

Comments
 (0)