In today's time, almost all web applications have a search bar feature, which is sometimes the first component a user interacts with on your page. This feature makes your app easier to use and helps users find what they're looking for more quickly and naturally. If you are wondering how to create such an advanced, dynamic, and responsive search box with ReactJS, this article is for you.
In this article, you will learn how to create an advanced search bar with suggestions in ReactJS. We will cover everything from setting up the project to implementing features like input handling, keyboard navigation, API fetching, highlighting search terms, and some more interesting features.
Follow the steps in this article and use the provided code. Trust me, by the end, you'll have a fully functional advanced search bar.
So let's get started and make the project like never before.
Table of contents:
- 1. Prerequisites
- 2. What Are We Going To Do in This Project
- 3. Setting Up The Project
- 4. Installing Dependencies for This Project
- 6. Creating the Project
-
- Step 1: Setting Up the Component
- Step 2: Define the SearchBar Component and State
- Step 3: Handle Input Change and Clear Search
- Step 4: Handle Keyboard Navigation
- Step 5: Fetch Data from the API
- Step 6: Update Display and Scroll Selected Item into View
- Step 7: Highlight Matching Text
- Step 8: Render the Component
- Step 9: Add Style to this Project
- Step 10: Integrate SearchBar Component into App.js
-
- 7. How to Filter Existing Data Using Search
For your convenience, you can find the complete source code for this project here. You can clone the repo and follow along with the easy-to-follow steps in this article.
Prerequisites
Before we start creating the project, make sure you have the following.
- You must have Node.js and npm installed on your system. If not, download Node.js from here. Installing Node.js will automatically install npm.
- You should know how React hooks and state management work.
- You should know how to use the Material-UI framework in your project.
- Knowledge of libraries like lodash.
What Are We Going To Do in this project?
In this project, we will create an advanced search bar using React. Imagine you're searching for "Apple" but you accidentally type "Ale." Normally, you'd get no results, right? But with our search bar, you can still find "Apple" even if you type "Ale." Cool, huh?
But wait! That's not all. Our search bar will also have some awesome features: autocomplete suggestions, filtering options, and a loading spinner to show when data is being fetched. Matching words will be shown in bold, you can navigate results with arrow keys, and hitting the enter button will take you to the relevant page, just like YouTube's search bar. If no results are found, a "No Result Found" message will appear.
Basically, there are two use cases for search bars on websites. First, we can search from an API like YouTube, where all auto-suggestions come from the server. The second use case is filtering existing data in the same application, like filtering records according to name. In this article, we will explore both types of search bars. First, we'll see how to show suggestions using an API. Second, we'll learn how to filter data within the app.
Trust me, you'll enjoy working on this project, and if you show it in an interview, your chances of landing a job will rise. So, let's start.
Setting Up The Project
First, make a new React project if you don't have one yet. Use the below command in your terminal to create a new project.
npx create-react-app advanced-search-bar // For create new project
cd advanced-search-bar // For navigate to your project directory
When you create a new React project with the command above, it sets up a folder structure. To simplify, keep only the index.html file in the public folder and delete the rest. In the src folder, keep App.js, and index.js, and delete the others.
When you remove these files, it doesn't mean they're useless. Each file has its own specific purpose, but for this project, our goal is simplicity.
Now, in the App.js and index.js, replace the code with the following:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
// src/App.js
import React from 'react'
function App() {
return (
<div>App</div>
)
}
export default App;
Now, let's install the dependencies we need for creating our Advanced Search Bar component.
Installing Dependencies for This Project
We'll need a few additional dependencies for our project. Install them using the command below.
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled lodash
Material-UI: This library provides pre-built components and icons to make our UI look nice and modern.
lodash: We use lodash in our project to debounce the input function. Debouncing helps to limit the rate at which a function gets executed, reducing unnecessary calls to the API while typing in the search bar.
If you understand this well now, that's great! If not, don't worry. By the end of this project, you'll fully understand why we use this library and what debouncing means.
So let's get started creating our project.
Project structure
Now, let's create the folders and files we need for this project.
- Navigate to the src folder in your project directory.
- Create a new folder named components inside the src folder.
- Inside the components folder, create two files named SearchBar.jsx and SearchBar.css.
After creating these files and folders, your project structure should look like this.
Creating the project
Step 1: Setting Up the Component
Inside the SearchBar.jsx file import the necessary dependencies.
//SearchBar.jsx
import { useState, useEffect, useRef } from 'react';
import { debounce } from 'lodash';
import SearchIcon from '@mui/icons-material/Search';
import CloseIcon from '@mui/icons-material/Close';
import CircularProgress from '@mui/material/CircularProgress';
import './SearchBar.css';
We started by importing essential React hooks: useState to manage state, useEffect to handle things like data fetching after the component renders, and useRef to directly access and manipulate DOM elements.
Next, we have imported the debounce function from the lodash library. When we type something in the input box, an API call is triggered for each letter, which is inefficient. To fix this, we used the debounce function. In this we can set a delay, so the API call happens only after a short period.
After, we imported two icons from Material UI: SearchIcon and CloseIcon. We also used CircularProgress to show a loading spinner while data is being fetched.
Lastly, we imported the SearchBar.css file to style our project.
Step 2: Define the SearchBar Component and State
Next, let's set up the SearchBar component and its state.
//SearchBar.jsx
function SearchBar() {
const [search, setSearch] = useState(''); // Manages the search input value
const [results, setResults] = useState([]); // Stores search results
const [selectedItem, setSelectedItem] = useState(-1); // Tracks the currently selected result
const [displaySearch, setDisplaySearch] = useState(''); // Manages what is shown in the search input
const [loading, setLoading] = useState(false); // Indicates if results are being fetched
const [noResults, setNoResults] = useState(false); // Shows if there are no results
const resultsRef = useRef([]); // Keeps references to result elements for scrolling and focus
search: These state variables manage the search input value, allowing the component to update as the user types.
results: These state variables store the fetched search results, enabling the UI to display the results dynamically.
selectedItem: These state variables track which search result is currently highlighted, supporting keyboard navigation within the results.
displaySearch: These state variables handle the displayed search value, ensuring the search input reflects the user’s selections.
loading: These state variables indicate whether search results are being fetched, triggering a loading spinner in the UI.
noResults: These state variables determine if there are no search results, allowing the UI to display a “No results found” message.
resultsRef: This ref holds references to the search result elements, enabling smooth scrolling and focus management for a better user experience.
Step 3: Handle Input Change and Clear Search
Next, we'll handle input changes and clear the search input.
//SearchBar.jsx
// Debounce the fetchResults function to prevent multiple requests
const debouncedFetchResults = useRef(
debounce((query) => {
fetchResults(query);
}, 500)
).current;
// Handles changes in the search input field
const handleChange = (e) => {
const value = e.target.value;
setSearch(value); // Update the search term
setDisplaySearch(value); // Update the displayed search term
setSelectedItem(-1); // Reset the selected item
if (value) {
debouncedFetchResults(value); // Call the debounced function if there's input
} else {
setResults([]); // Clear results if there's no input
setNoResults(false); // Reset the no results
}
};
// Clears the search input and resets state
const handleClose = () => {
setSearch(''); // Clear the search term
setDisplaySearch(''); // Clear the displayed search term
setResults([]); // Clear search results
setSelectedItem(-1); // Reset the selected item
setNoResults(false); // Reset the no results
}
In this code, we use a method called "debouncing" to make the search function work better. Instead of sending a request to the API for every letter typed by the user, the request is sent only after the user stops typing for 500 milliseconds. This way, the code doesn’t make too many requests while users typing.
Next, we have the handleChange function. It handles changes in the search input field. When the user types something, it updates the search term and the displayed search term and resets the selected item. If there is input, it calls the debounced function to fetch results. If no data is found, the no results found message will appear.
Next, we have the handleClose function. This function is triggered when the user clicks on the close icon. It clears the search input box, removes the search results, resets the selected item, and hides the "no results found" message.
Step 4: Handle Keyboard Navigation
Now, let's implement keyboard navigation functionality, so users can navigate through the search results using the arrow keys.
//SearchBar.jsx
const handleKeyDown = (e) => {
// Check if there are any results to navigate through
if (results.length > 0) {
// Move up in the list if the 'ArrowUp' key is pressed and an item is selected
if (e.key === 'ArrowUp' && selectedItem > 0) {
setSelectedItem((prev) => prev - 1);
}
// Move down in the list if the 'ArrowDown' key is pressed and not on the last item
else if (e.key === 'ArrowDown' && selectedItem < results.length - 1) {
setSelectedItem((prev) => prev + 1);
}
// Open the link of the selected item if the 'Enter' key is pressed and an item is selected
else if (e.key === 'Enter' && selectedItem >= 0) {
window.open(results[selectedItem].show.url);
}
}
};
For the keyboard navigation functionality, we have created the handleKeyDown function.
When the user presses the down arrow key, the function moves the selection to the next item in the list, if it’s not already at the last item. In the same way, if the user presses the up arrow key, it moves the selection to the previous item, if it’s not at the first item.
We also added the Enter key functionality so that when the user presses Enter, it opens the link for the currently selected item in the list.
Step 5: Fetch Data from the API
Create a function to fetch data from the API.
//SearchBar.jsx
const fetchResults = (searchTerm) => {
// Check if the search term is not empty
if (searchTerm !== '') {
// Show a loading spinner
setLoading(true);
// Fetch search results from the API
fetch(`https://api.tvmaze.com/search/shows?q=${searchTerm}`)
.then((res) => res.json()) // Convert the response to JSON
.then((data) => {
// Update state with search results
setResults(data);
// Hide the loading spinner
setLoading(false);
// Update the noResults state based on whether data is empty
setNoResults(data.length === 0);
})
.catch(() => {
// Hide the loading spinner if there's an error
setLoading(false);
});
} else {
// If the search term is empty, clear results and set noResults to false
setResults([]);
setNoResults(false);
}
};
The fetchResults function handles searching for shows using the TVmaze API. When called with a search term, it first checks if the term is not empty. If it’s valid, the function shows a loading spinner and then makes a request to the TVmaze API to fetch search results. It converts the API response to JSON format and updates the results accordingly.
Once the data is processed, the loading spinner is hidden. Then, it checks if any results were found and updates the 'No results found' message if necessary. If there’s an error during the fetch operation, it ensures that the loading spinner is hidden. If the search term is empty, it clears any existing results and hides the "No results found" message.
Step 6: Update Display and Scroll Selected Item into View
Update the display and scroll the selected item into view.
//SearchBar.jsx
useEffect(() => {
// Check if there is a valid selected item and if it has a show property
if (selectedItem >= 0 && results[selectedItem] && results[selectedItem].show) {
// Update the displaySearch state with the name of the selected show
setDisplaySearch(results[selectedItem].show.name);
// Scroll the selected item into view smoothly
if (resultsRef.current[selectedItem]) {
resultsRef.current[selectedItem].scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
}
}
}, [selectedItem, results]); // Dependency array: runs this effect when selectedItem or results change
We have created a useEffect function to handle updates when the selected item changes. Whenever a new item is selected, this function checks if the selected item is valid and has a show property. If so, it updates the search display with the name of the selected show. It also ensures that the selected item is smoothly scrolled into view so the user can see it clearly. This effect runs whenever the selectedItem or results change, keeping everything updated.
Step 7: Highlight Matching Text
Create a function to highlight matching text.
//SearchBar.jsx
const highlightText = (text, searchTerm) => {
// If there's no search term, return the text as is
if (!searchTerm) return text;
// Split the text into parts, separating the search term
const parts = text.split(new RegExp(`(${searchTerm})`, 'gi'));
// Map over the parts to highlight the search term
return parts.map((part, index) =>
part.toLowerCase() === searchTerm.toLowerCase() ? (
// If the part matches the search term, make it bold
<strong key={index} style={{ fontWeight: 'bold' }}>{part}</strong>
) : (
// Otherwise, just return the part as normal text
<span key={index}>{part}</span>
)
);
};
The highlightText function is used to highlight parts of a text that match a search term. This function works as follows: First, it checks if there’s a search term; if not, it returns the original text. Next, it splits the text into parts wherever the search term appears, creating an array of segments. Then, it goes through these segments: if a segment matches the search term (ignoring case), it wraps it in a <strong> tag to make it bold; if it doesn’t match, it wraps it in a <span> tag so the text appears in normal style.
Step 8: Render the Component
Next, render the SearchBar component.
return (
<section className='search_section'>
<div className="search_input_div">
<input
type="text"
className="search_input"
placeholder='What are you looking for?'
autoComplete='off'
onChange={handleChange}
value={displaySearch}
onKeyDown={handleKeyDown}
/>
<div className="search_icon">
{search === "" ? <SearchIcon /> : <CloseIcon onClick={handleClose} />}
</div>
</div>
<div className="search_result">
{loading && <div className="loading"><CircularProgress /></div>}
{!loading && noResults && <div className="no_results">No results found</div>}
{results.map((result, index) => (
result && result.show && (
<a
key={index}
href={result.show.url}
target="_blank"
rel="noopener noreferrer"
className={selectedItem === index ? 'search_suggestion_line active' : 'search_suggestion_line'}
ref={el => resultsRef.current[index] = el}
>
{highlightText(result.show.name, search)}
</a>
)
))}
</div>
</section>
);
}
export default SearchBar;
This code creates a SearchBar component with a text input for searching. It shows a search icon when the input is empty and a close icon when there's a search term. As users type, the search results are updated. If searching, a loading spinner appears; if no results are found, a "No results found" message is shown. The search results are displayed as clickable links, with the search term highlighted in the show names.
Step 9: Add Style to this Project
To improve the UI, we'll add some styles to the SearchBar.css file. Copy the code below and paste it into your SearchBar.css file. Make sure to import this style component into your SearchBar.jsx file. I've already done this, but double-check to be sure.
/* SearchBar.css */
body {
font-family: 'Trebuchet MS', sans-serif;
background-color: #a7c1e7;
margin: 0;
padding: 0;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.search_input::placeholder {
font-family: 'Trebuchet MS', sans-serif;
}
/* Search Section */
.search_section {
width: 400px;
margin: 20px;
padding: 15px;
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Input and Icon Container */
.search_input_div {
display: flex;
align-items: center;
position: relative;
}
.search_input {
width: 100%;
padding: 12px 10px;
border: none;
border-bottom: 1px solid #c7c7c7;
font-size: 16px;
outline: none;
font-family: 'Trebuchet MS', sans-serif;
}
.search_icon {
position: absolute;
right: 15px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.search_icon svg {
color: #aaa;
transition: 0.3s ease;
}
.search_icon svg:hover {
color: #7a7a7a;
}
/* Search Results */
.search_result {
margin-top: 10px;
max-height: 400px;
overflow-y: auto;
position: relative;
padding-right: 10px;
height: 300px; /* Fixed height */
}
/* Custom Scrollbar Styles */
.search_result::-webkit-scrollbar {
width: 10px;
}
.search_result::-webkit-scrollbar-thumb {
background: #bcbcbc;
border-radius: 10px;
}
.search_result::-webkit-scrollbar-thumb:hover {
background: #aeaeae;
}
.search_suggestion_line {
display: block;
padding: 12px 10px;
border-bottom: 1px solid rgb(198, 198, 198);
color: #333;
text-decoration: none;
margin-bottom: 5px;
transition:0.3s ;
word-wrap: break-word;
}
.search_suggestion_line:hover {
background-color: #007bff;
border-radius: 8px;
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.search_suggestion_line.active {
background-color: #007bff;
border-radius: 8px;
color: #ffffff;
}
/* Loading and No Results */
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 5px;
}
.no_results {
text-align: center;
padding: 10px;
color: #ff0000;
font-weight: bold;
}
/* Media Queries for Mobile Adaptation */
@media (max-width: 768px) {
.search_section {
padding: 15px;
max-width: 90%;
}
.search_input {
padding: 10px 15px;
font-size: 14px;
}
.search_suggestion_line {
padding: 10px 8px;
font-size: 14px;
}
.search_result {
padding-right: 5px;
}
}
Step 10: Integrate SearchBar Component into App.js
Now that we have our SearchBar.jsx component ready, let's integrate it into our main App.js file.
// App.js
import React from 'react';
import SearchBar from './components/SearchBar';
function App() {
return (
<div className="App">
<SearchBar />
</div>
);
}
export default App;
I hope you have successfully created the advanced search bar with suggestions without any errors. Your UI should look like the example below.
Now, you might be wondering how to get existing data in this same search bar. Let's see how we can do that.
How to Filter Existing Data Using Search
First, create a file called data.json file in the src folder in the same project. Put whatever data you want in this file. If you don't have any data, you can use the example data below to practice.
[
{
"id": 1,
"name": "Breaking Bad",
"description": "A high school chemistry teacher turned methamphetamine manufacturer."
},
{
"id": 2,
"name": "Stranger Things",
"description": "A group of kids uncover supernatural events in their small town."
},
{
"id": 3,
"name": "Game of Thrones",
"description": "Noble families vie for control of the Iron Throne in a fantasy world."
},
{
"id": 4,
"name": "The Office",
"description": "A mockumentary on a group of office workers."
},
{
"id": 5,
"name": "Friends",
"description": "Follows the lives of six friends living in New York City."
},
{
"id": 6,
"name": "The Crown",
"description": "The story of the reign of Queen Elizabeth II."
},
{
"id": 7,
"name": "Black Mirror",
"description": "An anthology series exploring a twisted, high-tech multiverse."
}
]
Next Import the JSON data at the top of your SearchBar.jsx file:
//SearchBar.jsx
import data from '../data.json' // Import the data
Next, replace the existing fetchResults function with the following code to filter the local data:
//SearchBar.jsx
const fetchResults = (searchTerm) => {
if (searchTerm !== '') {
setLoading(true);
const filteredResults = data.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
setResults(filteredResults);
setLoading(false);
setNoResults(filteredResults.length === 0);
} else {
setResults([]);
setNoResults(false);
}
};
Finally, in the return statement modify the results.map part to match the structure of your local data.
//SearchBar.jsx
{results.map((result, index) => (
<div
key={index}
className={selectedItem === index ? 'search_suggestion_line active' : 'search_suggestion_line'}
ref={el => resultsRef.current[index] = el}
>
{highlightText(result.name, search)}
</div>
))}
By making these changes, your SearchBar.jsx file will now filter data from the local data.json file instead of fetching it from an API.
You can clone the repo to access the complete source code: GitHub Repository.
Conclusion
We are sure that you would have easily understood and created this project without any errors. In this project, we have taught you how to set up your project, how to manage status, how to handle user input, how to retrieve data from API, how to reduce the number of API requests instead of sending one for each letter typed as well as How to Filter Existing Data Using Search.
Now you can easily implement this powerful feature in your project and if you want to add more features to enhance the user experience then you can implement recent search functionality so that users can quickly access their previous queries. You can also add a voice search feature so users can speak their search queries. When they search, the results will show up with images, making it more engaging.
If you have any questions about this article or related to web development, you can ask them in the question box given below, and you will get the answer promptly.