diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index 5541baa171..73b5e866f7 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Forward `disabled` state to hidden inputs in form-like components ([#3004](https://github.com/tailwindlabs/headlessui/pull/3004))
- Prefer incoming `data-*` attributes, over the ones set by Headless UI ([#3035](https://github.com/tailwindlabs/headlessui/pull/3035))
- Respect `selectedIndex` for controlled `` components ([#3037](https://github.com/tailwindlabs/headlessui/pull/3037))
+- Prevent unnecessary execution of the `displayValue` callback in the `ComboboxInput` component ([#3048](https://github.com/tailwindlabs/headlessui/pull/3048))
### Changed
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
index 562a33ea87..8e24d69d0e 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
@@ -489,6 +489,53 @@ describe('Rendering', () => {
})
)
+ it(
+ 'should keep the defaultValue when the Combobox state changes',
+ suppressConsoleLogs(async () => {
+ let data = [
+ { id: 1, name: 'alice', label: 'Alice' },
+ { id: 2, name: 'bob', label: 'Bob' },
+ { id: 3, name: 'charlie', label: 'Charlie' },
+ ]
+
+ function Example() {
+ let [person, setPerson] = useState(data[1])
+
+ return (
+
+ String(Math.random())} />
+
+
+ {data.map((person) => (
+
+ {person.label}
+
+ ))}
+
+
+ )
+ }
+
+ render()
+
+ let value = getComboboxInput()?.value
+
+ // Toggle the state a few times combobox
+ await click(getComboboxButton())
+ await click(getComboboxButton())
+ await click(getComboboxButton())
+
+ // Verify the value is still the same
+ expect(getComboboxInput()?.value).toBe(value)
+
+ // Choose an option, which should update the value
+ await click(getComboboxOptions()[2])
+
+ // Verify the value changed
+ expect(getComboboxInput()?.value).not.toBe(value)
+ })
+ )
+
it(
'should close the Combobox when the input is blurred',
suppressConsoleLogs(async () => {
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx
index 7971322b05..1c4c88f17a 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx
@@ -1005,7 +1005,7 @@ function InputFn<
// which should always result in a string (since we are filling in the value of the text input),
// you don't have to use this at all, a more common UI is a "tag" based UI, which you can render
// yourself using the selected option(s).
- let currentDisplayValue = (function () {
+ let currentDisplayValue = useMemo(() => {
if (typeof displayValue === 'function' && data.value !== undefined) {
return displayValue(data.value as unknown as TType) ?? ''
} else if (typeof data.value === 'string') {
@@ -1013,7 +1013,7 @@ function InputFn<
} else {
return ''
}
- })()
+ }, [data.value, displayValue])
// Syncing the input value has some rules attached to it to guarantee a smooth and expected user
// experience:
diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
index ee56f2d21d..d1e7772b79 100644
--- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
+++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
@@ -483,6 +483,48 @@ describe('Rendering', () => {
)
})
+ it(
+ 'should not crash when a defaultValue is not given',
+ suppressConsoleLogs(async () => {
+ let data = [
+ { id: 1, name: 'alice', label: 'Alice' },
+ { id: 2, name: 'bob', label: 'Bob' },
+ { id: 3, name: 'charlie', label: 'Charlie' },
+ ]
+
+ renderTemplate({
+ template: html`
+
+
+
+
+
+ {{ person.label }}
+
+
+
+ `,
+ setup: () => ({ person: ref(data[0]), data, displayValue: () => String(Math.random()) }),
+ })
+
+ let value = getComboboxInput()?.value
+
+ // Toggle the state a few times combobox
+ await click(getComboboxButton())
+ await click(getComboboxButton())
+ await click(getComboboxButton())
+
+ // Verify the value is still the same
+ expect(getComboboxInput()?.value).toBe(value)
+
+ // Choose an option, which should update the value
+ await click(getComboboxOptions()[1])
+
+ // Verify the value changed
+ expect(getComboboxInput()?.value).not.toBe(value)
+ })
+ )
+
it(
'should not crash when a defaultValue is not given',
suppressConsoleLogs(async () => {