Multi Select Search Using ReactJS
Multi-Select Search Using ReactJS enables users to search for and select multiple items from a list simultaneously, enhancing usability and efficiency in interfaces like forms or tag selection. React interviews often include challenges related to building interactive and user-friendly components. One such common task is creating a multi-select search functionality. This article will guide you through a practical example of implementing a multi-select search component using React.
Prerequisites:
Approach to implement Multi Select Search:
The goal is to create a multi-select search component where users can type a query, see suggestions, and select multiple options. The selected options will be displayed as pills, and users can remove them individually.
Steps to Create the Project:
- Step 1: Set Up Your React App with Vite:
npm create vite@latest
- Step 2: Navigate to the Project Directory
cd multi-select-search
- Step 3: Install the package dependency.
npm install
Project Structure:

The updated dependencies in package.json file will look like:
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"web-vitals": "^2.1.4"
}
/* App.css */
body {
font-family: sans-serif;
}
.user-search-container {
display: flex;
position: relative;
}
.user-search-input {
width: 100%;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
padding: 5px;
border: 1px solid #ccc;
border-radius: 20px;
}
.user-search-input input {
border: none;
height: 20px;
padding: 5px;
}
.user-search-input input:focus {
outline: none;
}
.suggestions-list {
max-height: 300px;
overflow-y: scroll;
list-style: none;
padding: 0;
margin: 0;
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
}
.suggestions-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
cursor: pointer;
border-bottom: 1px solid #ccc;
}
.suggestions-list li:last-child {
border-bottom: none;
}
.suggestions-list li:hover {
background-color: #ccc;
}
.suggestions-list li img {
height: 20px;
}
.user-pill {
height: 20px;
display: flex;
align-items: center;
gap: 5px;
background-color: black;
color: #fff;
padding: 5px 10px;
border-radius: 16px;
cursor: pointer;
}
.user-pill img {
height: 100%;
}
.suggestions-list li.active {
background-color: #ccc;
}
// App.jsx
import { useEffect, useRef, useState } from "react";
import "./App.css";
import Pill from "./components/Pill";
function App() {
const [searchTerm, setSearchTerm] = useState("");
const [suggestions, setSuggestions] = useState([]);
const [selectedUsers, setSelectedUsers] = useState([]);
const [selectedUserSet, setSelectedUserSet] = useState(new Set());
const [activeSuggestion, setActiveSuggestion] = useState(0);
const inputRef = useRef(null);
useEffect(() => {
const fetchUsers = () => {
setActiveSuggestion(0);
if (searchTerm.trim() === "") {
setSuggestions([]);
return;
}
fetch(`https://dummyjson.com/users/search?q=${searchTerm}`)
.then((res) => res.json())
.then((data) => setSuggestions(data))
.catch((err) => {
console.error(err);
});
};
fetchUsers();
}, [searchTerm]);
const handleSelectUser = (user) => {
setSelectedUsers([...selectedUsers, user]);
setSelectedUserSet(new Set([...selectedUserSet, user.email]));
setSearchTerm("");
setSuggestions([]);
inputRef.current.focus();
};
const handleRemoveUser = (user) => {
const updatedUsers = selectedUsers.filter(
(selectedUser) => selectedUser.id !== user.id
);
setSelectedUsers(updatedUsers);
const updatedEmails = new Set(selectedUserSet);
updatedEmails.delete(user.email);
setSelectedUserSet(updatedEmails);
};
const handleKeyDown = (e) => {
if (
e.key === "Backspace" &&
e.target.value === "" &&
selectedUsers.length > 0
) {
const lastUser = selectedUsers[selectedUsers.length - 1];
handleRemoveUser(lastUser);
setSuggestions([]);
} else if (e.key === "ArrowDown" &&
suggestions?.users?.length > 0) {
e.preventDefault();
setActiveSuggestion((prevIndex) =>
prevIndex < suggestions.users.length - 1 ?
prevIndex + 1 : prevIndex
);
} else if (e.key === "ArrowUp" &&
suggestions?.users?.length > 0) {
e.preventDefault();
setActiveSuggestion((prevIndex) =>
(prevIndex > 0 ? prevIndex - 1 : 0));
} else if (
e.key === "Enter" &&
activeSuggestion >= 0 &&
activeSuggestion < suggestions.users.length
) {
handleSelectUser(suggestions.users[activeSuggestion]);
}
};
return (
<div className="user-search-container">
<div className="user-search-input">
{/* Pills */}
{selectedUsers.map((user) => {
return (
<Pill
key={user.email}
image={user.image}
text={`${user.firstName} ${user.lastName}`}
onClick={() => handleRemoveUser(user)}
/>
);
})}
{/* input feild with search suggestions */}
<div>
<input
ref={inputRef}
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search For a User..."
onKeyDown={handleKeyDown}
/>
{/* Search Suggestions */}
{searchTerm && (
<ul className="suggestions-list">
{suggestions?.users?.map((user, index) => {
return !selectedUserSet.has(user.email) ? (
<li
className={index === activeSuggestion ?
"active" : ""}
key={user.email}
onClick={() => handleSelectUser(user)}
>
<img
src={user.image}
alt={`${user.firstName}
${user.lastName}`}
/>
<span>
{user.firstName} {user.lastName}
</span>
</li>
) : (
<></>
);
})}
</ul>
)}
</div>
</div>
</div>
);
}
export default App;
// main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// Pill.jsx
const Pill = ({ image, text, onClick }) => {
return (
<span className="user-pill" onClick={onClick}>
<img src={image} alt={text} />
<span>{text} ×</span>
</span>
);
};
export default Pill;
Steps to run the application:
npm run dev
Output: