[Fixed] Shadcn Select is having issue with dynamically generated SelectItem – Select

by
Alexei Petrov
onselect react-hook-form reactjs zod

Quick Fix: In SelectItem component, ensure the value property is a string. Convert non-string values to strings explicitly before assigning them to value. This will resolve issues with dynamic generation of SelectItem components.

The Problem:

There’s an issue with Shadcn library’s Select component. SelectItem generated dynamically are not getting displayed in the SelectValue box, although the selection works fine and the Zod validation library is getting the selected value correctly. However, using static values for SelectItem works as expected.

The Solutions:

Solution 1: Type Conversion

The issue in this case is that the `value` prop of the `SelectItem` component expects a string value, but you’re passing it a number (the `id` of the property type). This is why the selected item is not being displayed in the `SelectValue` box.

To fix this, you need to convert the id to a string before passing it to the value prop. This can be done using the toString() method, like this:

propTypes.map((type) => (
  <SelectItem key={type.id} value={type.id.toString()}>
    {type.name}
  </SelectItem>
));

With this change, the selected item will be displayed correctly in the `SelectValue` box.

Solution 2: Add `value={field.value}` to the `Select` component

The original code missed providing a value prop to the <Select> component. This resulted in the dynamically generated SelectItem values not being displayed in the SelectValue box.

To fix this, add value={field.value} to the <Select> component, like this:

<Select
  onValueChange={field.onChange}
  defaultValue={field.value}
  value={field.value}
>
  ...
</Select>

With this change, the <Select> component will now receive the current value from the form, and the dynamically generated SelectItem values will be displayed correctly in the SelectValue box. Here’s an example:

const ProductForm: React.FC<any> = ({ product }) => {
  const form = useForm<ProductFormType>({
    resolver: zodResolver(ProductFormSchema),
    defaultValues: {
      categoryId: "",
    },
    values: {
      categoryId: product.categoryId, // initial value
    },
  });

  async function onSubmit(values: ProductFormType) {
    // handle Submit
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="categoryId"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Category</FormLabel>
              <Select
                onValueChange={field.onChange}
                defaultValue={field.value}
                value={field.value}
              >
                <FormControl>
                  <SelectTrigger>
                    <SelectValue placeholder="Select a category" />
                  </SelectTrigger>
                </FormControl>
                <SelectContent>
                  {categories.map((category) => (
                    <SelectItem key={category.id} value={category.id}>
                      {category.name}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">submit</Button>
      </form>
    </Form>
  );
};

By adding value={field.value} to the <Select> component, you ensure that the selected value is always displayed in the SelectValue box, even for dynamically generated SelectItem values.

Solution 3: Using conditional rendering around FormField

In this solution, the issue is that dynamic values are not fetched before the form is rendered, causing the default value to be empty. To fix this, conditional rendering is used around the `FormField` to ensure that it is only rendered after the dynamic values are fetched.

Steps involved:

  1. Create a state variable to track whether the dynamic values have been fetched.
  2. Fetch the dynamic values and update the state variable when they are available.
  3. Use useEffect hook to check if the state variable has changed and conditionally render the FormField based on that.

Example code:

const [valuesFetched, setValuesFetched] = useState(false);

useEffect(() => {
  // Fetch dynamic values and update state variable
  fetchDynamicValues().then(() => {
    setValuesFetched(true);
  });
}, []);

return (
  {valuesFetched && (
    <FormField
      control={form.control}
      name="type"
      render={({ field }) => (
        <FormItem className="grid grid-cols-4 items-center gap-4">
          <FormLabel className="sm:block sm:text-right">Type</FormLabel>
          <Select
            disabled={isSubmitting}
            onValueChange={field.onChange}
            defaultValue={field.value}
          >
            <FormControl>
              <SelectTrigger className="col-span-4 sm:col-span-3">
                <SelectValue placeholder="Select a property type" />
              </SelectTrigger>
            </FormControl>
            <SelectContent className="SelectContent">
              {propTypes.map((type) => (
                <SelectItem key={type.id} value={type.id}>
                  {type.name}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </FormItem>
      )}
    />
  )}
);

In this code:

  • The valuesFetched state variable is used to track whether the dynamic values have been fetched.
  • The useEffect hook is used to fetch the dynamic values and update the valuesFetched state variable when they are available.
  • The FormField is conditionally rendered using the && operator, which checks if the valuesFetched state variable is true. This ensures that the FormField is only rendered after the dynamic values have been fetched.

\n

Solution 4: Use find() to retrieve and set the default value

\n

To resolve the issue with dynamic SelectItems in the Shadcn React library, you can utilize the find() method to retrieve the appropriate element from the propTypes array and assign it as the default value for the Select component. This approach ensures that the selected value is displayed in the SelectValue box. Here’s an improved explanation of the solution:

  1. Identify the Issue:

    • The problem arises because the default value passed to the Select component is a plain value (e.g., field.value.type), which doesn’t match the structure of the SelectItem options. Dynamically generated SelectItems require the default value to be an object with a value property that matches the values of the options.
  2. Implement the Solution:

    • Utilize the find() method on the propTypes array to search for the element that matches the field.value.type value. This retrieves the corresponding SelectItem object from the array.
    • Assign the retrieved SelectItem object to the defaultValue prop of the Select component. This ensures that the default value is an object with the correct structure, allowing the selected value to be displayed in the SelectValue box.
  3. Adjusted Code:

    • Here’s the revised code for the Select component:
<Select
  disabled={isSubmitting}
  onValueChange={field.onChange}
  defaultValue={propTypes.find(type => type.id === field.value.type)?.type}
>
  <FormControl>
    <SelectTrigger className="col-span-4 sm:col-span-3">
      <SelectValue placeholder="Select a property type" />
    </SelectTrigger>
  </FormControl>
  <SelectContent className="SelectContent">
    {propTypes.map((type) => (
      <SelectItem key={type.id} value={type.id}>
        {type.name}
      </SelectItem>
    ))}
  </SelectContent>
</Select>
  1. Explanation of the Adjusted Code:

    • The find() method is used to search for the SelectItem object that matches the field.value.type value within the propTypes array. The found object is assigned to the defaultValue prop, ensuring that the Select component receives the correct default value.
    • The defaultValue is now an object with the value property set to the id of the selected SelectItem, enabling the selected value to be displayed appropriately.
  2. Benefits of this Solution:

    • This approach allows you to dynamically generate SelectItems and set the default value correctly, resolving the issue with empty SelectValue boxes.
    • It ensures that the selected value is displayed in the SelectValue box, improving the user experience and providing a clear indication of the selected option.

Solution 5: Add `type.name` in value while creating `SelectItem`

The reason behind this issue is that the value prop of the SelectItem component in the Select component is not properly set. In the provided code, the value prop is set to type.id, which is the unique identifier for each property type. However, the SelectValue component expects the value prop to be the actual value of the selected option, which in this case is the type.name.

To fix this issue, the value prop should be set to type.name instead of type.id. This will ensure that the SelectValue component displays the correct value when an option is selected. Here’s the updated code:

{propTypes.map((type) => (
  <SelectItem key={type.id} value={type.name}>
    {type.name}
  </SelectItem>
))}

With this change, the Select component will properly display the selected property type in the SelectValue component, and the values will be correctly submitted to the form.

Q&A

Can SelectItem’s value be an object?

No, the value of SelectItem must be a string.

What is the correct way to set the default value of the Select component?

Use defaultValue prop with the correct value type.

Is it possible to conditionally render the FormField component?

Yes, by using conditional rendering around the FormField component.

Video Explanation:

The following video, titled "Handle Dropdown In Playwright And Verify Dropdown Values ...", provides additional insights and in-depth exploration related to the topics discussed in this post.

Play video

Playwright provides a handy method called selectOption which allows us to select values using label, value, and index ... Create your testRigor ...