primitives: [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?

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 25
  • Comments: 22 (1 by maintainers)

Commits related to this issue

Most upvoted comments

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

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

  1. Create a key, and have a controlled value
const [key, setKey] = useState<number>(+new Date())
const [value, setValue] = useState<string>()
  1. Assign key to Select.Root
<Select.Root
  key={key}
  value={value}
  onValueChange={setValue}
  className='relative'
>
  {/* rest of select */}
</Select.Root>
  1. 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.

@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>

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.

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

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.Item with value="".

@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>

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.

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.

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.

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.

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”.