radix-ui / primitives

Radix Primitives is an open-source UI component library for building high-quality, accessible design systems and web apps. Maintained by @workos.
https://radix-ui.com/primitives
MIT License
15.34k stars 765 forks source link

Cannot dynamically set select value to non-existent item #2817

Open BrendanC23 opened 5 months ago

BrendanC23 commented 5 months ago

Bug report

Current Behavior

I have code like the following. When a button is clicked, a value in a form is updated. There is a select whose options come from the form. This involves an API request to get the new options, and so it is done asynchronously using TRPC.

const schema = {
    name: z.string(),
}

const defaultValues = {
    name: "",
}

function App() {
    const form = useForm({
        resolver: zodResolver(schema),
        defaultValues,
        mode: "onSubmit"
    });

    function onClick() {
        form.setValue("name", "A");
    }

    return (
        <form>
            <Button onClick={onClick}>Click</Button>
            <SelectInput form={form} />
        </form>
    )
}

function SelectInput({form}) {
    const name = form.watch("name");
    const result = trpc.items.useQuery({ name }); // When this resolves, it will contain "A"
    const items = result.data || [];

    return (
        <Select>
            <SelectTrigger>
                <SelectValue placeholder="Select" />
            </SelectTrigger>
            <SelectContent>
                {items.map((item) => (
                <SelectItem value={item} key={item}>
                    {item}
                </SelectItem>
                ))}
            </SelectContent>
        </Select>
    )
}

This leads to the following:

  1. Click button to set form name to "A"
  2. Select re-renders, triggering an asynchronous TRPC query
  3. While the query is resolving, there are no select items. This causes this effect to run, which sets the value of the form back to "".
  4. TRPC query finishes, populating the items. However, the form value has been reset by Radix-select.

Expected behavior

I want to be able to set the value of the select input to something that is not a current select item. As a workaround currently, I can return early if the name isn't filled in, but that is not ideal.

Reproducible example

This stackblitz is a simplified example of the above, using useTimeout instead of TRPC to set the options.

https://codesandbox.io/p/devbox/trusting-hill-wqjlvq

This example uses Shadcn-ui, which uses Radix select. The select has options of "A", "B", and "C". When the button is clicked to set the value to "A", the select updates properly.

"D" is not in the list of items. When the button is clicked to set the value to "D", a timeout fires to update the list of options. If you look at the console, "Changed" is logged with an empty string. This is because the BubbleSelect change event fires.

If the "Set to D" button is clicked again, it works because "D" is part of the options list.

Suggested solution

This is caused by this code:

https://github.com/radix-ui/primitives/blob/b32a93318cdfce383c2eec095710d35ffbd33a1c/packages/react/select/src/Select.tsx#L1556-L1577

When the select value is changed to "D", the form value is properly updated for a moment. Then, this effect runs, and sets the value back to an empty string. I'm not sure what needs to change here to let this work.

Additional context

n/a

Your environment

Software Name(s) Version
Radix Package(s) @radix-ui/react-select ^2.0.0
React n/a ^18.2.0
Browser Chrome 123.0.6312.105
Assistive tech n/a n/a
Node n/a 20.0.9
npm/yarn npm 9.8.1
Operating System Windows 10
Kingsmanam commented 2 months ago

same here

sofly commented 1 month ago

the same here