primitives
primitives copied to clipboard
[Select] Unable to clear value and return to `placeholder`
I'm playing with the new Select placeholder prop, and am wondering if the following is possible?
As an example, let's say you have a series of 3 selects for an automobile and the placeholder values for each are "make", "model", and "year". When you select a new "make" (after having already selected both a "make" and a "model") and the underlying list for "model" is updated, the component still technically has a value based on the previous list's selection, therefore the placeholder doesn't render.
Is there a way to reset value when updating the list?
Hey @dungle-scrubs I imagine this is all possible via controlled props though I'm not 100% clear on what you're describing, would you be able to provide a sandbox showing your attempted solution?
Hi @andy-hook I'll try to get a working example but it might take a day or two (sorry about that, I'm out of time right now). In the meantime, I can describe it better:
<Select.Root>
<Select.Trigger>
<Select.Value placeholder="make"/>
</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.Viewport>
{apiResponse.makes?.map((item, i) => {
return (
<Select.Item value={item} key={i}>
<Select.ItemText>{item.text}</Select.ItemText>
</Select.Item>
)
})}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
<Select.Root>
<Select.Trigger>
<Select.Value placeholder="model"/>
</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.Viewport>
{apiResponse.models?.map((item, i) => {
return (
<Select.Item value={item} key={i}>
<Select.ItemText>{item.text}</Select.ItemText>
</Select.Item>
)
})}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
<Select.Root>
<Select.Trigger>
<Select.Value placeholder="year"/>
</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.Viewport>
{apiResponse.years?.map((item, i) => {
return (
<Select.Item value={item} key={i}>
<Select.ItemText>{item.text}</Select.ItemText>
</Select.Item>
)
})}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
Looking at the placeholder values, these only render if value or defaultValue are not set.
If you've already selected a "model", but then apiResponse.models changes and a different list is rendered, the Select still holds the previously selected value (and that value might not correspond to an item in the new api response).
So at that point in time, if the items have changed, I would expect value to reset so that placeholder can render "model" again.
Thinking about this further though, I think this could be solved on my end with the logic, "if the data set is empty, set the value as undefined".
@andy-hook Same with me ... When I select value then clear value to undefined SelectPrimitive still show previous value
<SelectPrimitive.Trigger asChild aria-label={ariaLabel}>
<Button>
<SelectPrimitive.Value
placeholder={
<span className="text-primary-500">{placeholder}</span>
}
/>
<SelectPrimitive.Icon className="ml-2">
{allowClear ? (
<span
onPointerDown={(e) => {
e.preventDefault();
e.stopPropagation();
props.onValueChange(undefined);
}}
>
<Cross2Icon width={14} height={14} />
</span>
) : (
<CaretSortIcon width={14} height={14} />
)}
</SelectPrimitive.Icon>
</Button>
</SelectPrimitive.Trigger>
Thanks both for the examples, I can confirm this on my side. I'm going to mark this as a bug to look into.
In the meantime one workaround could be to forego placeholder and instead render an option with value="" and control / clear to it using setValue('')
Bumping this, as its a pretty huge roadblock for us. A native "clear" functionality would be appreciated, but at a minimum, setting value to null/undefined needs to be possible to truly call this controlled.
Is there any update on this issue?
I no longer think this is an issue as I just learned something about React I didn't know before.
Taking a look at the new beta docs, under the section, "Resetting all state when a prop changes", it turns out that if you add a key prop to any component, React will reset all of the state inside of that component and all of its children when that key changes.
Taking that advice, I made this sandbox where you can add/remove that key prop to see the effect.
Btw, In those docs, the page, "You Might Not Need an Effect" has really changed the way I think about and build components.
Cheers
AFAICT, the internal Radix useControllableState hook switches between controlled and uncontrolled through value === undefined:
https://github.com/radix-ui/primitives/blob/91763d2ed7d84e03e0e6f1307a7a29d9c7b04433/packages/react/use-controllable-state/src/useControllableState.tsx#L17-L19
So that means that once you've given the value any non-undefined value, it is impossible to go back to undefined, as that will only cause the component to go to uncontrolled mode, and use the last value before undefined as its current value.
Moreover, you also can't just pass value={null}, because the placeholder check also uses a value === undefined check:
https://github.com/radix-ui/primitives/blob/91763d2ed7d84e03e0e6f1307a7a29d9c7b04433/packages/react/select/src/Select.tsx#L337
The only workaround I can see is the aforementioned one with changing the key to force full re-creation of the component and its hooks lifecycle.
I've just ran into this myself, attempting to select a filter on a page search and then attempting to clear that filter. Unfortunate!
Edit: Here's my solution
- Create a key, and have a controlled value
const [key, setKey] = useState<number>(+new Date())
const [value, setValue] = useState<string>()
- Assign key to
Select.Root
<Select.Root
key={key}
value={value}
onValueChange={setValue}
className='relative'
>
{/* rest of select */}
</Select.Root>
- Add clear button within
Select.Root
<button
className='absolute transform -translate-y-1/2 right-3 top-1/2'
type='button'
onClick={e => {
e.stopPropagation()
setValue(undefined)
setKey(+new Date())
}}
>
<XMarkIcon />
</button>
As suggested by cprecioso this forces the select to fully re-render. In my case I'm using react-hook-form to control the value of the select- dumbed down example above.
I've also run into a similar issue, while using remix, of not being able to reset the placeholder after the form submission. I'm getting a formRef and resetting it after submission, but the Select doesn't reset. I found that adding a key to the Form component and resetting this after form submission did the trick. So similar to these other solutions but at the Form component level.
Don't know it it is still needed but just add a dummy value :
<SelectItem value={"none"}>DDD</SelectItem> and revert back to it.
The select component assumes it's being uncontrolled when value is undefined, which makes it effectively impossible to introduce an empty state in a controlled way. Ie. if you use some state that is initially undefined and do some validation on callback to onValueChange which may prevent the outside state from changing, it won't work: the select will update itself, even though value prop stays undefined.
Solution for that would be to allow null as value which indicates an empty state in a controlled way.
I believe it's a bug. Workarounds proposed in this issue don't work for me, as changing the key to rerender the entire component does not make sense in my case (and is not an idiomatic way to do things in react and impairs performance).
I tried introducing a dummy value and styling it like a placeholder, but it appears in the dropdown next to real items. If I use styles to hide it, the dropdown cannot position itself correctly.
I've created a PR which forces the placeholder appears when the selected value doesn't match with any items.
Therefore, passing an empty string or dummy value will work as expected.
The select component assumes it's being uncontrolled when value is
undefined, which makes it effectively impossible to introduce an empty state in a controlled way. Ie. if you use some state that is initiallyundefinedand do some validation on callback toonValueChangewhich may prevent the outside state from changing, it won't work: the select will update itself, even though value prop staysundefined.Solution for that would be to allow
nullas value which indicates an empty state in a controlled way.I believe it's a bug. Workarounds proposed in this issue don't work for me, as changing the key to rerender the entire component does not make sense in my case (and is not an idiomatic way to do things in react and impairs performance).
I tried introducing a dummy value and styling it like a placeholder, but it appears in the dropdown next to real items. If I use styles to hide it, the dropdown cannot position itself correctly.
I'm struggling with same thing right now and found that null works as a charm to clear the field, but typescript doesn't aproves that, have you found another workaround ? Or will wait @SonMooSans 's PR ?
If you don't mind about it, just simply add @ts-ignore. (Since you know what you're doing)
The problem is that null doesn't display the placeholder, it might be kinda "hacky" to replace <Select.Value /> with the placeholder when value is null. That's why the PR was created.
If you don't mind about it, just simply add
@ts-ignore. (Since you know what you're doing)The problem is that null doesn't display the placeholder, it might be kinda "hacky" to replace
<Select.Value />with the placeholder when value is null. That's why the PR was created.
Well observed, I got some select's with and without placeholder and tested only the without one's, like you said, it's gonna be really hacky, hope they take on soon your PR
Hey all, thank you for all your feedback on this. I have been working on a PR for this: #2174, let me know if you see any objections.
Note this will be technically a major because it could be a breaking change if people have resolved to use a
Select.Itemwithvalue="".
Hi, I encountered a similar problem with react-hook-form and resolved it by creating a custom hook specifically for this issue
function useValueKey(value: string | undefined | null): string | number {
const [prevValue, setPrevValue] = useState(value)
const [key, setKey] = useState(0)
if (value !== prevValue) {
setPrevValue(value)
setKey(k => k + 1)
}
return key
}
// use case
const key = useValueKey(props.value)
// assign key to the corresponding values.
Hi, I encountered a similar problem with react-hook-form and resolved it by creating a custom hook specifically for this issue
function useValueKey(value: string | undefined | null): string | number { const [prevValue, setPrevValue] = useState(value) const [key, setKey] = useState(0) if (value !== prevValue) { setPrevValue(value) setKey(k => k + 1) } return key } // use case const key = useValueKey(props.value) // assign key to the corresponding values.
In v2, you can just reset to "" and the placeholder will be displayed
@dextermb , thanks for the example https://github.com/radix-ui/primitives/issues/1569#issuecomment-1434801848. Following resets controlled select input to its default value. Using react-hook-form.
const fieldValue = form.watch('fieldName')
<Select.Root
key={fieldValue}
...
>
{/* rest of select */}
</Select.Root>
In v2, you can just reset to
""and the placeholder will be displayed
Thank you!
Using react-hook-form works for me:
export interface Props {
...
onChange(value: string): void;
value?: string;
}
export function SelectInput(props: Props) {
return (
<Select.Root
onValueChange={(value) => {
props.onChange(value);
}}
value={props.value || ""}
>
In the component:
<SelectInput
...
onChange={(value) => setValue("gender", value)}
value={watch("gender")}
/>
function clearFilter() {
...
setValue("gender", undefined)
}