react-spectrum: Setting Picker Item key prop causes keys to be coerced to a string regardless of the actual type

Provide a general summary of the issue here

The Picker component supports items with numeric id / key props.

Item.key unset

If the Item.key prop is not explicitly set, keys get derived from the id or key props on the items array.

e.g.

function Example() {
  let items = [
    { id: 1, name: 'Aardvark' },
    { id: 2, name: 'Cat' },
    { id: 3, name: 'Dog' },  ];
    
  let [animalId, setAnimalId] = React.useState(null);

  return (
    <>
      <Picker
        label="Pick an animal"
        items={items}
        onSelectionChange={setAnimalId}
        value={animalId}
      >
        { /* Item.key not explicitly set */
          (item) => <Item>{item.name}</Item>
        }
      </Picker>
      <p>Animal id type: {typeof animalId}</p>
    </>
  );
}

The onSelectionChange handler will pass a numeric key to setAnimalId (consistent with the items data) and value + defaultValue will work with values of type number.

Item.key explicitly set

If the Item.key prop is explicitly set e.g.

<Item key={item.id}>{item.name}</Item>}

The onSelectionChange will pass a string id to setAnimalId, and the value + defaultValue props will require values of type string in order to work.

🤔 Expected Behavior?

I would expect in the case of setting Item.key prop explicitly:

  • onSelectionChange should pass the actual value (not the stringified version) of the original id / key
  • value, and defaultValue should be able to match the id / key prop on the items instead of requiring them to be string values

😯 Current Behavior

When Item.key prop is explicitly set

  • onSelectionChange passes a stringified value of the original id / key
  • value, and defaultValue don’t match the items id / key prop unless they are stringified

💁 Possible Solution

No response

🔦 Context

We have a wrapper component that needs to explicitly set the Item.key prop based on properties on the items. In cases where these props are numeric or boolean types, this forces the input value + onSelectedValue change handlers to work with strings which doesn’t match the original data.

🖥️ Steps to Reproduce

https://codesandbox.io/p/sandbox/modern-shadow-znqpwf?file=%2Fsrc%2FApp.js%3A22%2C25&layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clu8m4b1r00063b6qt0em6lmr%2522%252C%2522sizes%2522%253A%255B100%252C0%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clu8m4b1r00023b6qa3ncq69w%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clu8m4b1r00033b6qnvq8g9lk%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clu8m4b1r00053b6q8rg0tzb0%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B50%252C50%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clu8m4b1r00023b6qa3ncq69w%2522%253A%257B%2522id%2522%253A%2522clu8m4b1r00023b6qa3ncq69w%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clu8m4rnj00513b6q8rkyrj65%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A22%252C%2522startColumn%2522%253A25%252C%2522endLineNumber%2522%253A22%252C%2522endColumn%2522%253A25%257D%255D%252C%2522filepath%2522%253A%2522%252Fsrc%252FApp.js%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522activeTabId%2522%253A%2522clu8m4rnj00513b6q8rkyrj65%2522%257D%252C%2522clu8m4b1r00053b6q8rg0tzb0%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clu8m4b1r00043b6qt6yqaeqs%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A0%252C%2522path%2522%253A%2522%252F%2522%257D%255D%252C%2522id%2522%253A%2522clu8m4b1r00053b6q8rg0tzb0%2522%252C%2522activeTabId%2522%253A%2522clu8m4b1r00043b6qt6yqaeqs%2522%257D%252C%2522clu8m4b1r00033b6qnvq8g9lk%2522%253A%257B%2522tabs%2522%253A%255B%255D%252C%2522id%2522%253A%2522clu8m4b1r00033b6qnvq8g9lk%2522%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Afalse%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D

Version

3.34.1

What browsers are you seeing the problem on?

Chrome

If other, please specify.

No response

What operating system are you using?

MacOS

🧢 Your Company/Team

No response

🕷 Tracking Issue

No response

About this issue

  • Original URL
  • State: open
  • Created 3 months ago
  • Comments: 15 (10 by maintainers)

Most upvoted comments

@snowystinger that would probably work but for my use case that would require mapping over the entire list any time an item changes which seems not ideal since our lists could be 10K-100K items.

Not ideal, but theoretically your items aren’t changing that frequently. So if you’re running into an issue, you could speed this up by doing your own caching so that you don’t have {...item, key: item.fakeKey} for every item. Also, this happens in linear time.

@sookmax agreed this may be nice; I’m bringing it up with the team this week, hopefully.

Also of note for anyone coming across this, we do change this in the RAC version to use the id prop instead of the key prop. https://github.com/adobe/react-spectrum/blob/e6923d9473264b972974513dacbcfab5a24d5616/packages/react-aria-components/src/Collection.tsx#L677 apologies for the confusion.

I might be terribly wrong here, but I think it has to do with the fact that the key provided to <Item> is essentially the underlying React Component’s key, and it’s extracted as-is in CollectionBuilder:

https://github.com/adobe/react-spectrum/blob/1293adf00a11ecac84ec9ab19af7dcea970af2b2/packages/%40react-stately/collections/src/CollectionBuilder.ts#L127

https://github.com/adobe/react-spectrum/blob/1293adf00a11ecac84ec9ab19af7dcea970af2b2/packages/%40react-stately/collections/src/CollectionBuilder.ts#L64-L84

Above, element passed as the first argument of this.getkey() is the object representation of <Item key={item.id}> (in our context) containing the provided key, and we extract this React key verbatim in the first if statement of getKey().

Thing is, React always seems to convert the provided key to string. (I found this stack exchange answer). For example if you specify something like <Item key={{hello: 'world'}}>, you’ll see the element object would be of form: {$$typeof: Symbol(react.element), key: '[object Object]', ref: null, props: {…}, type: ƒ, …}