DEV Community

Kush Bhandari
Kush Bhandari

Posted on

Custom Multi Select in React

Machine Coding Interview Question
Build a Multi-Select Dropdown with Search (Tag Input)

This is VERY frequently asked in frontend interviews.

📌 Problem Statement

Build a Multi-Select Dropdown component like this:

[ React × ] [ JavaScript × ] [ + Add ]

Clicking "Add" opens a dropdown with searchable options.

📦 Data
const options = [
"React",
"JavaScript",
"TypeScript",
"Node.js",
"Next.js",
"HTML",
"CSS"
];

✅ Requirements
1️⃣ Multi Select

User can select multiple options

Selected items appear as tags

[ React × ] [ CSS × ]
2️⃣ Remove Tag

Each tag has ❌

Clicking removes it

3️⃣ Dropdown Toggle

Clicking input or button opens dropdown

Clicking outside closes it

4️⃣ Search Filter

Typing filters options:

Input: "re"
→ React
5️⃣ Prevent Duplicate Selection

Already selected items should:

Not appear in dropdown OR

Be disabled

6️⃣ Keyboard Support (Important)

Enter → select highlighted item

Backspace → remove last tag (if input empty)

⚠️ Constraints (Interview Critical)

❌ No libraries

❌ No mutation of data

✅ Proper state modeling

✅ Event handling (click outside!)

❌ Avoid unnecessary re-renders

🎯 Edge Cases Interviewers Test
Case Expected Behavior
Rapid typing Filter works smoothly
Duplicate select Prevented
Backspace Removes last tag
Click outside Closes dropdown
No results Show "No options"

import { useEffect, useMemo, useRef, useState } from "react";

const options = [
  "React",
  "JavaScript",
  "TypeScript",
  "Node.js",
  "Next.js",
  "HTML",
  "CSS"
];

export default function MultiSelect() {
  const [query, setQuery] = useState("");
  const [selected, setSelected] = useState([]);
  const [isOpen, setIsOpen] = useState(false);

  const ref = useRef(null);

  // click outside
  useEffect(() => {
    function handleClickOutside(e) {
      if (ref.current && !ref.current.contains(e.target)) {
        setIsOpen(false);
      }
    }

    document.addEventListener("mousedown", handleClickOutside);
    return () =>
      document.removeEventListener("mousedown", handleClickOutside);
  }, []);

  const filtered = useMemo(() => {
    return options
      .filter((opt) => !selected.includes(opt))
      .filter((opt) =>
        opt.toLowerCase().includes(query.toLowerCase())
      );
  }, [query, selected]);

  const handleSelect = (item) => {
    setSelected((prev) => [...prev, item]);
    setQuery("");
  };

  const handleRemove = (item) => {
    setSelected((prev) => prev.filter((i) => i !== item));
  };

  return (
    <div ref={ref} style={{ width: 300 }}>
      <div
        style={{ border: "1px solid black", padding: 8 }}
        onClick={() => setIsOpen(true)}
      >
        {selected.map((item) => (
          <span key={item} style={{ marginRight: 4 }}>
            {item}
            <button onClick={() => handleRemove(item)}></button>
          </span>
        ))}

        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search..."
        />
      </div>

      {isOpen && (
        <ul style={{ border: "1px solid gray", marginTop: 4 }}>
          {filtered.length > 0 ? (
            filtered.map((item) => (
              <li
                key={item}
                onClick={() => handleSelect(item)}
                style={{ cursor: "pointer" }}
              >
                {item}
              </li>
            ))
          ) : (
            <li>No options</li>
          )}
        </ul>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)