Skip to content

Fix Unexpected undefined crash in Combobox component with virtual mode #3678

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 4, 2025

Conversation

RobinMalfait
Copy link
Member

@RobinMalfait RobinMalfait commented Apr 4, 2025

This PR fixes an issue where the Combobox component crashes if you are using the virtual option and you quickly type something such that the Combobox opens but no valid options are available.

We already check if the current active index is available in the internal options list. However, if you then call virtualizer.scrollToIndex(data.activeOptionIndex) it will crash if you are too fast.

Screen.Recording.2025-04-04.at.12.53.40.mov

If you are typing slowly, then it will work as expected.

Screen.Recording.2025-04-04.at.12.54.01.mov

I did find an open issue on TanStack's repo about this: TanStack/virtual#879

This PR is basically a workaround by delaying the call. But it does have the expected behavior now.

Screen.Recording.2025-04-04.at.12.57.40.mov

Fixes: #3583

We already verify that the `activeOptionIndex` is inside the `options`
before we call the `virtualizer.scrollToIndex` function.

However, if you do this when the virtualizer isn't ready internally yet,
then you get an error like `unexpected undefined`.

Found an open issue on their GitHub: TanStack/virtual#879

Don't know if there is a better fix we can apply here, but delaying the
`scrollToIndex` does seem to fix the problem.

Another thing I tried is waiting for the DOM to be ready, but we already
only run this code _if_ the DOM `el` is available.
Copy link

vercel bot commented Apr 4, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
headlessui-react ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 4, 2025 10:57am
headlessui-vue ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 4, 2025 10:57am

@RobinMalfait RobinMalfait merged commit 9d3b0ff into main Apr 4, 2025
8 checks passed
@RobinMalfait RobinMalfait deleted the fix/issue-3583 branch April 4, 2025 12:16
RobinMalfait added a commit that referenced this pull request Apr 17, 2025
This PR improves the performance of the `Combobox` component. This is a
similar implementation as:

- #3685
- #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 #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:

- #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: 96f4da7

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
RobinMalfait added a commit that referenced this pull request Apr 17, 2025
This PR improves the performance of the `Combobox` component. This is a
similar implementation as:

- #3685
- #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 #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:

- #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: 96f4da7

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
RobinMalfait added a commit that referenced this pull request May 20, 2025
This PR fixes an issue with the `Combobox` component when using the
`virtual` mode.

We recently already fixed this issue
(#3678) by applying a
patch on the `@tanstack/virtual-core` package. But this was only applied
in cjs builds, not in the esm builds.

Instead of trying to fix it in our build process, we decided to just fix
it upstream (TanStack/virtual#1004) and this PR
bumps the version of `@tanstack/virtual-core` to the latest version
(which includes the fix).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

unexpected undefined using combobox
2 participants