[Select] Improve generality of Select
See original GitHub issueLet’s look at an example use of of Select
to illustrate a problem.
Say I have an array of objects like this that I’d like to use in my Select
component:
const locations = [
{id: 4, info: {city: 'San Francisco', state: 'CA', someHash: '123' } }
{id: 5, info: {city: 'San Francisco', state: 'CA', someHash: '456' } }
]
I want to display the items, combining together city
and state
, and pass along someHash
to my onChange event handler whenever it changes. So I write this in my app:
const SelectLocation = ({onChange}) => {
const options = locations.map(location =>
({ label: location.info.city + ', ' + location.info.state , value: location.id }));
return (<Select
options={options}
labelKey="label"
valueKey="value"
onChange={ ({value}) => locations.find(location => location.id === value).someHash }
/>);
}
Because Select
needs a flat object to read the labelKey
and valueKey
from, I’ve added an O(n) operation and to create a new array of objects in each render. And because we only pass the value, I now have to do an O(n) lookup to find my object. (Actually, it looks like Select
passes the whole object. But the types for the component do not reflect this.)
In fact, this is an issue any time you want to show a select of nested objects, or objects without certain existing properties!
How can we improve this? Let’s make Select totally agnostic to what the user passes in.
const SelectLocation2 = ({onChange}) => (
<Select
options={locations}
label={location => location.info.name + ', ' + location.info.state}
onChange={(location) => onChange(location.info.someHash)} />)
Not we only have to call the label fn when we actually render that option. Select
can also cache the results of this fn as a possible performance improvement.
Currently, the type of Select’s Props is:
export type OptionT = $ReadOnly<{
id?: string | number,
label?: React.Node,
...
}>;
type PropsT = {
onChange: ({value: OptionT}) => mixed,
options: $ReadOnlyArray<OptionT>,
value: OptionT,
valueKey: string,
labelKey: string
}
Even if Select
can currently accept objects of any type, Flow will give us the wrong object shape.
The type for Select
should be this (note the type param for Option
rather than a hard-coded shape):
type PropsT<Option> = {
onChange: ({value: Option}) => mixed,
label: Option => React.Node,
options: $ReadOnlyArray<Option>,
value: Option
}
Implementing this would be a breaking change, but you could make upgrading easier by providing a façade like this:
type LegacyPropsT<Option: {}> = {
options: $ReadOnlyArray<Option>,
valueKey: $Keys<Option>,
labelKey: $Keys<Option>,
onChange: ({value: Option}) => mixed
}
const LegacySelect = <Option: {}>({options, id, label}: LegacyPropsT<Option>) => {
return <NewSelect
id={p => p[id]}
label{p => p[label]} />
}
We can even improve the accuracy of the types of the legacy wrapper by making it parametric, restricting valueKey
and labelKey
to the keys that exist on options
This is a problem with Select
, but it’s also a problem for any other Base Web component that takes in a list of options. This same pattern (making the list of options
parametric) would improve these components as well.
This API has a couple other benefits, notably solving some issues with how StatefulSelect
deals with mapping the initialState
value
to an option. The new type of StatefulSelect
’s props could be:
type NewStatefulSelectProps<Option> = {
label: Option => React.Node,
initialState: { value: Option },
....
}
We can deal with the identity of the option
directly rather than have to map back and forth between option
and valueKey
.
I want to get some feedback on this proposed API change, but I’d be happy to help make these changes if we can agree that they’re beneficial.
Edit: I see newer versions of baseweb’s Select
have getOptionLabel
and getOptionValue
, which are sort of in the correct spirit, but also have overly-specialized types. So maybe this issue should be about fixing the types, and removing valueKey
and labelKey
(since they artificially restrict the option param, and cause the mapping issue for StatefulSelect
).
Issue Analytics
- State:
- Created 4 years ago
- Reactions:3
- Comments:10 (4 by maintainers)
This is not stale! I have a PR incoming
Closing for now - please feel free to reopen if you plan to work on this! 😃
For reference, we’ve split the Select component since the creation of this ticket, and added a warning for value prop misuse: https://github.com/uber/baseweb/commit/1e6d1276645523b5e2020297668194f83cc551d5