themes icon indicating copy to clipboard operation
themes copied to clipboard

Invalid `Select` value in the `form` entries

Open rago4 opened this issue 2 years ago • 4 comments

Hey! First up, thank you for your hard work on Radix UI, it's a great library and I love working with it. Now, let's get straight to the point. Recently I've been playing around with Select component and I found a strange behaviour. Basically, the value seems to be invalid once retrieved by using form.entries() after submitting <form>. In the documentation, I saw that every example of <Select.Root> uses defaultValue instead of value but I need it to be dynamic as I'm setting the initial value by using search parameters. Also, I could simply use value from React state and call it a day, but I need it in a Remix action (server side), so it won't do the job here 😅

Here's the code to reproduction:

import { useState } from 'react';
import { Button, Select } from '@radix-ui/themes';

const colors = [
  { value: 'red', label: 'Color red' },
  { value: 'green', label: 'Color green' },
  { value: 'blue', label: 'Color blue' },
];

export default function App() {
  const [color, setColor] = useState('');

  return (
    <div>
      <form
        onSubmit={(event) => {
          event.preventDefault();
          const form = new FormData(event.currentTarget);
          // without state change: the value is "red", should be ""
          // after state change the value is assigned correctly
          // after clearing selection with "Clear" button and submitting: the value is undefined, should be ""
          console.log(Object.fromEntries(form.entries()));
        }}
      >
        <Select.Root name="color" value={color} onValueChange={setColor}>
          <Select.Trigger placeholder="Select a color" />
          <Select.Content>
            <Select.Group>
              {colors.map((color) => (
                <Select.Item key={color.value} value={color.value}>
                  {color.label}
                </Select.Item>
              ))}
            </Select.Group>
          </Select.Content>
        </Select.Root>
        <Button type="submit">Submit</Button>
        <Button type="button" variant="outline" onClick={() => setColor('')}>
          Clear
        </Button>
      </form>
    </div>
  );
}

And here's the link to the live demo I've prepared: https://stackblitz.com/edit/vitejs-vite-w3x9aw?file=src%2FApp.tsx&terminal=dev

Thank you once again and sorry if I missed something and this turns out to be expected behaviour 😅

rago4 avatar Jan 13 '24 17:01 rago4

Hi, @rago4,

The issue you're experiencing is due to how the Radix UI Select component handles its value. The Select component doesn't behave like a traditional HTML select element, and it doesn't directly interact with the form it's in. This means that when you submit the form, the value of the Select component isn't automatically included in the form data.

To work around this, you can create a hidden input field in your form that holds the value of the Select component. When the form is submitted, the value of the hidden input field will be included in the form data.

Here's how you can modify your code to include a hidden input field:

import { useState, useCallback } from 'react';
import { Button, Select } from '@radix-ui/themes';
import { v4 as uuidv4 } from 'uuid';

const colors = [
  { value: 'red', label: 'Color red' },
  { value: 'green', label: 'Color green' },
  { value: 'blue', label: 'Color blue' },
];

export default function App() {
  const [color, setColor] = useState('');
  const [key, setKey] = useState(uuidv4());

  const reset = useCallback(() => {
    setColor('');
    setKey(uuidv4()); // Change key to reset Select component
  }, []);

  const handleSubmit = useCallback((event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const form = new FormData(event.currentTarget);
    console.log(Object.fromEntries(form.entries()));
  }, []);

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <Select.Root key={key} name="color" onValueChange={setColor}>
          <Select.Trigger placeholder="Select a color" />
          <Select.Content>
            <Select.Group>
              {colors.map((color) => (
                <Select.Item key={color.value} value={color.value}>
                  {color.label}
                </Select.Item>
              ))}
            </Select.Group>
          </Select.Content>
        </Select.Root>
        <input type="hidden" name="color" value={color} />
        <Button type="submit">Submit</Button>
        <Button type="button" variant="outline" onClick={reset}>
          Clear
        </Button>
      </form>
    </div>
  );
}

In this code, I've added a hidden input field with the name color and the value set to the current color state. When the form is submitted, the value of this hidden input field will be included in the form data.

onurhan1337 avatar Feb 03 '24 10:02 onurhan1337

Actually, Using a UUID for the key in this specific case could be considered overengineering. The key just needs to change to trigger a re-render of the component, it doesn't need to be globally unique or follow the UUID format.

Using Math.random() is perfectly fine in this case, as the likelihood of generating the same number twice in a row is extremely low.

So, you can stick with your original approach: I've gave an example with uuid.

onurhan1337 avatar Feb 03 '24 10:02 onurhan1337

The Select component should natively work with forms out of the box—we'll take a look

vladmoroz avatar Feb 05 '24 14:02 vladmoroz