Robin Malfait 97cb20806a Improve Combobox component performance (#3697)
This PR improves the performance of the `Combobox` component. This is a
similar implementation as:

- https://github.com/tailwindlabs/headlessui/pull/3685
- https://github.com/tailwindlabs/headlessui/pull/3688

Before this PR, the `Combobox` component is built in a way where all the
state lives in the `Combobox` itself. If state changes, everything
re-renders and re-computes the necessary derived state.

However, if you have a 1000 items, then every time the active item
changes, all 1000 items have to re-render.

To solve this, we can move the state outside of the `Combobox`
component, and "subscribe" to state changes using the `useSlice` hook
introduced in https://github.com/tailwindlabs/headlessui/pull/3684.

This will allow us to subscribe to a slice of the state, and only
re-render if the computed slice actually changes.

If the active item changes, only 3 things will happen:

1. The `ComboboxOptions` will re-render and have an updated
`aria-activedescendant`
2. The `ComboboxOption` that _was_ active, will re-render and the
`data-focus` attribute wil be removed.
3. The `ComboboxOption` that is now active, will re-render and the
`data-focus` attribute wil be added.

The `Combobox` component already has a `virtual` option if you want to
render many many more items. This is a bit of a different model where
all the options are passed in via an array instead of rendering all
`ComboboxOption` components immediately.

Because of this, I didn't want to batch the registration of the options
as part of this PR (similar to what we do in the `Menu` and `Listbox`)
because it behaves differently compared to what mode you are using
(virtual or not). Since not all components will be rendered, batching
the registration until everything is registered doesn't really make
sense in the general case. However, it does make sense in non-virtual
mode. But because of this difference, I didn't want to implement this as
part of this PR and increase the complexity of the PR even more.

Instead I will follow up with more PRs with more improvements. But the
key improvement of looking at the slice of the data is what makes the
biggest impact. This also means that we can do another release once this
is merged.

Last but not least, recently we fixed a bug where the `Combobox` in
`virtual` mode could crash if you search for an item that doesn't exist.
To solve it, we implemented a workaround in:

- https://github.com/tailwindlabs/headlessui/pull/3678

Which used a double `requestAnimationFrame` to delay the scrolling to
the item. While this solved this issue, this also caused visual flicker
when holding down your arrow keys.

I also fixed it in this PR by introducing `patch-package` and work
around the issue in the `@tanstack/virtual-core` package itself.

More info: 96f4da70b16d5cf259643

Before:


https://github.com/user-attachments/assets/132520d3-b4d6-42f9-9152-57427de20639

After:


https://github.com/user-attachments/assets/41f198fe-9326-42d1-a09f-077b60a9f65d

## Test plan

1. All tests still pass
2. Tested this in the browser with a 1000 items. In the videos below the
only thing I'm doing is holding down the `ArrowDown` key.

Before:


https://github.com/user-attachments/assets/945692a3-96e6-4ac7-bee0-36a1fd89172b

After:


https://github.com/user-attachments/assets/98a151d0-16cc-4823-811c-fcee0019937a
2025-04-17 15:16:30 +02:00
2025-02-27 14:56:04 +01:00
2020-09-16 18:19:33 +02:00
2022-01-31 12:29:27 +01:00
2020-09-16 18:19:33 +02:00
2023-01-01 06:25:25 -05:00

Headless UI

A set of completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.


Documentation

For full documentation, visit headlessui.com.

Installing the latest version

You can install the latest version by using:

  • npm install @headlessui/react@latest
  • npm install @headlessui/vue@latest

Installing the insiders version

You can install the insiders version (which points to whatever the latest commit on the main branch is) by using:

  • npm install @headlessui/react@insiders
  • npm install @headlessui/vue@insiders

Note: The insiders build doesn't follow semver and therefore doesn't guarantee that the APIs will be the same once they are released.

Packages

Name Version Downloads
@headlessui/react npm version npm downloads
@headlessui/vue npm version npm downloads
@headlessui/tailwindcss npm version npm downloads

Community

For help, discussion about best practices, or any other conversation that would benefit from being searchable:

Discuss Headless UI on GitHub

For casual chit-chat with others using the library:

Join the Tailwind CSS Discord Server

Contributing

If you're interested in contributing to Headless UI, please read our contributing docs before submitting a pull request.

S
Description
Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.
Readme 6.5 MiB
Languages
TypeScript 95.3%
Vue 4.3%
JavaScript 0.3%
Shell 0.1%