Build a Reusable Multi-Select Dropdown in React with ShadCN & React Hook Form

73

image downloaded from here

Description

In this blog, we will walk through creating a reusable multi-select dropdown component in React using ShadCN’s UI library and integrating it with react-hook-form for seamless form handling. This component will support dynamic options and validations, making it perfect for any project requiring multiple selections.


Why This Component?

When building forms, having reusable components that integrate well with validation libraries like react-hook-form is crucial. By the end of this guide, you’ll have a versatile multi-select dropdown that:


Step 1: Setup Your Project

Before starting, make sure your project is set up with React, react-hook-form, and ShadCN components. If not, you can install the required dependencies:

npm install react-hook-form zod shadcn-ui

Step 2: Define the Options Format

Let’s assume your options look like this:

const categories = [
  { name: 'JavaScript', id: 'cm4yeqerr0002v5lgvzkr1v35', slug: 'javascript' },
  { name: 'Python', id: 'cm4yeqeve0009v5lgw7rmcvo7', slug: 'python' },
  { name: 'Ruby', id: 'cm4yeqeve0009v5lgw7rmcvo9', slug: 'ruby' },
];

Each category has a name, id, and slug. We’ll use the id to track selections.


Step 3: Create the Multi-Select Dropdown Component

Here is the implementation of the MultiSelectDropdown component:

"use client";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";

const MultiSelectDropdown = ({
  options,
  value,
  onChange,
  placeholder = "Select options",
}) => {
  const handleSelect = (option) => {
    const isSelected = value.includes(option.id);
    if (isSelected) {
      onChange(value.filter((id) => id !== option.id));
    } else {
      onChange([...value, option.id]);
    }
  };

  const handleClearAll = () => {
    onChange([]);
  };

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" className="w-full justify-between" asChild>
          {value.length > 0 ? (
            <div className="flex flex-wrap gap-1">
              {value.map((id) => {
                const selectedOption = options.find(
                  (option) => option.id === id
                );
                return (
                  <Badge key={id} className="flex items-center gap-2">
                    {selectedOption?.name}
                    <button
                      onClick={(e) => {
                        e.stopPropagation();
                        // handleSelect(selectedOption);
                        onChange(value.filter((valueId) => valueId !== id));
                      }}
                    >
                      ✕
                    </button>
                  </Badge>
                );
              })}
              {/* <Button
                onClick={handleClearAll}
                className="cursor-pointer text-red-500"
              >
                Clear All
              </Button> */}
            </div>
          ) : (
            <span className="text-gray-500">{placeholder}</span>
          )}
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent className="w-full max-h-60 overflow-auto">
        {options.map((option) => (
          <DropdownMenuItem
            key={option.id}
            onClick={() => handleSelect(option)}
            className={`cursor-pointer ${
              value.includes(option.id) ? "bg-gray-500" : ""
            }`}
          >
            <span>{option.name}</span>
          </DropdownMenuItem>
        ))}
        {value.length > 0 && (
          <DropdownMenuItem
            onClick={handleClearAll}
            className="cursor-pointer text-red-500"
          >
            Clear All
          </DropdownMenuItem>
        )}
      </DropdownMenuContent>
    </DropdownMenu>
  );
};
export default MultiSelectDropdown;

Step 4: Integrate with React Hook Form

Here’s how you can integrate the dropdown component into your form:

Form Setup

"use client";
import React from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { z } from "zod";
import { useForm } from "react-hook-form";
import MultiSelectDropdown from "../ui/multi-select";

const formSchema = z.object({
  
  categories: z
    .array(z.string())
    .min(1, { message: "please atleast select a value" }),
});

function CourseMainForm({  categories }) {
  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: {
      categories: []
    },
  });
  const onSubmit = async (data) => {
    console.log(data);
    
  };
  return (
    <div>
      <h1>Fill all details for course </h1>
      <Form {...form}>
        <form
          onSubmit={form.handleSubmit(onSubmit)}
          className=" flex  flex-col gap-4"
        >

          <FormField
            control={form.control}
            name="categories"
            render={({ field }) => (
              <FormItem>
                <FormLabel>categories</FormLabel>
                <FormControl>
                  <MultiSelectDropdown
                    options={categories} // Array of options
                    value={field.value} // Current value from react-hook-form
                    onChange={field.onChange} // Update form value
                    placeholder="Select categories"
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <Button type="submit">Submit</Button>
        </form>
      </Form>
    </div>
  );
}

export default CourseMainForm;

Step 5: Adding Default Values

If you want to set default selected values, ensure your useForm default values include valid IDs for the categories field:

defaultValues: {
  categories: ["cm4yeqeve0009v5lgw7rmcvo7"],
}

Conclusion

With this implementation, you now have a reusable multi-select dropdown component that integrates smoothly with react-hook-form. This component can be used across various forms and adapted for different types of data. Feel free to customize and expand its functionality to suit your needs.

💥 Did you find this blog helpful? 💥

If you enjoyed this post, please clap and follow for more insights on web development and Next.js! Your support helps me continue sharing useful content to enhance your development journey. 🚀

FrontendUI/UXNext.jsWeb DevelopmentTailwindCSS
Sagar Sangwan

Written by Sagar Sangwan

Code. Write. Build. Explore. 💻✍️ Software developer by day, mechanical tinkerer by night. When I’m not shipping code or writing blogs, you’ll find me trekking up a mountain, whipping up a feast, or hitting the open road on two wheels. Life is better in high gear.

Follow

View more blogs by me CLICK HERE

Loading related blogs...

Stay Updated

Subscribe to get the latest posts delivered to your inbox