Add by
prop for Listbox
, Combobox
and RadioGroup
#1482
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
The problem
Currently our components with "data" can also contain objects. Up until now we always required that you maintained object equality because we are using
a === b
orlist.includes(a)
.This isn't always easy to do because it can happen that an object is coming from an API or is refreshed and now you will have multiple identities thus different objects.
The Workarounds
In the example below, you can see that we are referencing
people[1]
as theselectedPerson
, this works since the objectpeople
is defined outside of the component so that all objects can be stable.import { useState } from 'react' import { Listbox } from '@headlessui/react' const people = [ { id: 1, name: 'Durward Reynolds', unavailable: false }, { id: 2, name: 'Kenton Towne', unavailable: false }, { id: 3, name: 'Therese Wunsch', unavailable: false }, { id: 4, name: 'Benedict Kessler', unavailable: true }, { id: 5, name: 'Katelyn Rohan', unavailable: false }, ] function MyListbox() { const [selectedPerson, setSelectedPerson] = useState( + people[1] ) return ( <Listbox value={selectedPerson} onChange={setSelectedPerson}> <Listbox.Button>{selectedPerson.name}</Listbox.Button> <Listbox.Options> {people.map((person) => ( <Listbox.Option key={person.id} value={person} disabled={person.unavailable} > {person.name} </Listbox.Option> ))} </Listbox.Options> </Listbox> ) }
If the
people
array was coming from an API, then this can't be guaranteed. A solution up until now was to use theid
for the actual value, like this:The Solution
This PR tries to solve this by introducing a
by
prop. You can think of it like "Compare the objects by id".import { useState } from 'react' import { Listbox } from '@headlessui/react' function MyListbox() { const people = [ { id: 1, name: 'Durward Reynolds', unavailable: false }, { id: 2, name: 'Kenton Towne', unavailable: false }, { id: 3, name: 'Therese Wunsch', unavailable: false }, { id: 4, name: 'Benedict Kessler', unavailable: true }, { id: 5, name: 'Katelyn Rohan', unavailable: false }, ] const [selectedPerson, setSelectedPerson] = useState({ id: 2, name: 'Kenton Towne', unavailable: false }) return ( <Listbox value={selectedPerson} onChange={setSelectedPerson} + by="id" > <Listbox.Button>{selectedPerson.name}</Listbox.Button> <Listbox.Options> {people.map((person) => ( <Listbox.Option key={person.id} value={person} disabled={person.unavailable} > {person.name} </Listbox.Option> ))} </Listbox.Options> </Listbox> ) }
This will compare all objects based on the
"id"
you provided in theby
prop. The advantage of this is that you can use objects from anywhere and yourselectedPerson
stays an object instead of an id.The
by
prop is a very simple property lookup, however if you need more control:Then you can also provide a function to the
by
prop, this function has the following signature:import { useState } from 'react' import { Listbox } from '@headlessui/react' function MyListbox() { const people = [ { id: 1, name: 'Durward Reynolds', unavailable: false }, { id: 2, name: 'Kenton Towne', unavailable: false }, { id: 3, name: 'Therese Wunsch', unavailable: false }, { id: 4, name: 'Benedict Kessler', unavailable: true }, { id: 5, name: 'Katelyn Rohan', unavailable: false }, ] const [selectedPerson, setSelectedPerson] = useState({ id: 2, name: 'Kenton Towne', unavailable: false }) return ( <Listbox value={selectedPerson} onChange={setSelectedPerson} + by={(a, z) => a.id === z.id} > <Listbox.Button>{selectedPerson.name}</Listbox.Button> <Listbox.Options> {people.map((person) => ( <Listbox.Option key={person.id} value={person} disabled={person.unavailable} > {person.name} </Listbox.Option> ))} </Listbox.Options> </Listbox> ) }