Skip to content
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

Add by prop for Listbox, Combobox and RadioGroup #1482

Merged
merged 2 commits into from
May 20, 2022

Conversation

RobinMalfait
Copy link
Member

@RobinMalfait RobinMalfait commented May 20, 2022

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 or list.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 the selectedPerson, this works since the object people 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 the id for the actual value, like this:

  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(people[1])
+   const [selectedPersonId, setSelectedPersonId] = useState({  id: 2, name: 'Kenton Towne', unavailable: false }) // Notice that this is a different object

    return (
      <Listbox value={selectedPersonId} onChange={setSelectedPersonId}>
        <Listbox.Button>
-         {selectedPerson.name}
+         {people.find(person => person.id === selectedPerson).name} // Yikes, this makes it a little awkward
        </Listbox.Button>
        <Listbox.Options>
          {people.map((person) => (
            <Listbox.Option
              key={person.id}
-             value={person}
+             value={person.id}
              disabled={person.unavailable}
            >
              {person.name}
            </Listbox.Option>
          ))}
        </Listbox.Options>
      </Listbox>
    )
  }

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 the by prop. The advantage of this is that you can use objects from anywhere and your selectedPerson stays an object instead of an id.

The by prop is a very simple property lookup, however if you need more control:

  • because you want to do a deep equal
  • or you want to compare (deeply) nested objects

Then you can also provide a function to the by prop, this function has the following signature:

declare function compare<T>(a: T, z: T): boolean
  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>
    )
  }

@vercel
Copy link

vercel bot commented May 20, 2022

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

Name Status Preview Updated
headlessui-react ✅ Ready (Inspect) Visit Preview May 20, 2022 at 8:26PM (UTC)
headlessui-vue ✅ Ready (Inspect) Visit Preview May 20, 2022 at 8:26PM (UTC)

@ginzatron
Copy link

Would this work for a Listbox with the multiple prop on it?

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.

2 participants