Table of Contents
1. Introduction
2. Requirements
3. Step 1: Setting Up the Project
4. Step 2: Creating the Input
5. Step 3: Rendering the list of items
6. Step 4: Building the filtering functionality
7. Step 5: Getting the items from an API and filtering them
8. Step 6: Refactoring a little (Bonus)
8.1. ItemList Component
8.2. Input Component
8.3. API call
9. Conclusion
Introduction
When I began working with React, one of the common challenges I encountered was implementing a real-time search filter functionality. This feature updates the displayed items as the user types and shows all the items again if the search filter is empty. So in this tutorial, I will guide you through the steps to create this feature in React. We'll start with a list of hard-coded items and then proceed to a list of items obtained from an API.
By the end of this tutorial, you'll have a solid understanding of how to build this valuable feature. We’ll begin by implementing the complete functionality in App.jsx and then refactor it into reusable components.
Let's get started!
Requirements
- Npm and Node.js installed
- Basic React Knowledge
Step 1: Setting Up the Project
To set up the project, we will be using Vite. So open your terminal and execute the following commands:
npm create vite search-filter --template react
Feel free to replace “search-filter” with any name you prefer for your project. This should be enough to create the project. However, if you come across any options in your command-line interface, make sure to select “React” when prompted to choose a framework, and “Javascript” when asked to select a variant.
Now, navigate into the project directory and install the required packages by executing the following commands:
cd search-filter
npm install
After that, you can run the command npm run dev
. You should be able to open the project at http://localhost:5173/
and see something like this:
Step 2: Creating the Input
Now, open the project in your preferred IDE (I personally use VSCode), and navigate to the src/App.jsx
file. Delete its contents to start with a clean slate. If you prefer to work without the default styles, you can remove the import './index.css'
line from the src/main.jsx
file. You may also choose to leave it intact if you want to keep the default styles. In my case, I will remove it.
We’ll begin by building the entire functionality within App,jsx
, and we can refactor it later into reusable components.
First let’s create our search input with its proper state to control its value:
// src/App.jsx
import { useState } from 'react'
function App() {
const [searchItem, setSearchItem] = useState('')
const handleInputChange = (e) => {
const searchTerm = e.target.value;
setSearchItem(searchTerm)
}
return (
<div>
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
</div>
)
}
export default App
With this, we have implemented a functional input. We’re setting its value to the searchItem
state, which updates whenever the user types in the input thanks to the onChange
event handler. We could simply use the setSearchItem
function directly in the input, but we’re doing it like this because we’re adding some more functionality to the handler function soon.
You should have something like this:
Step 3: Rendering the list of items
Next, let’s add a list of items and render it below the input.
// src/App.jsx
import { useState } from 'react'
const users = [
{ firstName: "John", id: 1 },
{ firstName: "Emily", id: 2 },
{ firstName: "Michael", id: 3 },
{ firstName: "Sarah", id: 4 },
{ firstName: "David", id: 5 },
{ firstName: "Jessica", id: 6 },
{ firstName: "Daniel", id: 7 },
{ firstName: "Olivia", id: 8 },
{ firstName: "Matthew", id: 9 },
{ firstName: "Sophia", id: 10 }
]
function App() {
const [searchItem, setSearchItem] = useState('')
const handleInputChange = (e) => {
const searchTerm = e.target.value;
setSearchItem(searchTerm)
}
return (
<>
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
<ul>
{users.map(user => <li key={user.id}>{user.firstName}</li>)}
</ul>
</>
)
}
export default App
Perfect, now we have a list of items which we’re storing in the users
variable and then we’re using the map method to loop through the list and render each item using a li
tag.
You should be seeing something like this:
Step 4: Building the filtering functionality
Great! We have our input and we have our items, now let’s get to the fun part 👀 We need to change the list of items being rendered depending on what the user types in the input. For that, we need to do 3 things:
- Add a state to save the filtered items, and set it to the users variable,
- On the event handler, we need to filter the items depending on what the user is writing and set the result to the filtered items state
- Render the filtered items instead of the users variable
So let’s get to it:
// src/App.jsx
//... users variable and imports
function App() {
const [searchItem, setSearchItem] = useState('')
const [filteredUsers, setFilteredUsers] = useState(users)
const handleInputChange = (e) => {
const searchTerm = e.target.value;
setSearchItem(searchTerm)
const filteredItems = users.filter((user) =>
user.firstName.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredUsers(filteredItems);
}
return (
<>
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
<ul>
{filteredUsers.map(user => <li key={user.id}>{user.firstName}</li>)}
</ul>
</>
)
}
export default App
A common mistake I used to make here is that when filtering the items in the handler function, I used the filtered items state (filteredUsers
) instead of the variable that contained all the items (users
), which led to the unwanted behavior of the filter working when you first start typing on the input but then if you erase some characters the items don’t get filtered again, and if you erase all the characters you don’t see the whole list of items again.
This happens because when the component first loads, filteredUsers
initial state contains all the users, but when you start typing, it gets updated to a new array of users that matches the search input value, and then it can never access the complete list of users again.
So it’s really important that when you do the filtering, you always do it using the variable that contains all the items.
This is working great right now, you should have something like this:
But in real life, it’s more common that you need to get the list of items from an API an then filter those items, so let’s see how we can do that.
Step 5: Getting the items from an API and filtering them
Let’s use the DummyJSON API to get the users. We need to fetch the users when the component loads (we can use an useEffect
for that) save them to a state and then render them on the list. Well, we already have the filteredUsers
state, and that’s the state that we’re using to render the users, so maybe we can store the users gotten from the API there and filter those same users, right?
Well… no, this is another common mistake that I used to make 😕. If we do that, it would be the same mistake I mentioned above, when the component first loads, the filteredUsers
state is empty, then the useEffect
gets executed and the users are fetched from the API, set to the filteredUsers
state and then rendered, now you have the complete list of items in filteredUsers
so you use that to do the filtering, but when you start typing, that state gets updated and it can never access to the complete list of users again.
So what can we do? well, the solution is to create another state, let’s call it apiUsers
, that will hold the complete list of users when the component first loads, it will act kinda like the users
variable we initially had. Then we use apiUsers
, instead of filteredUsers
to filter the items when the user types on the input. We no longer need the previous users
variable so we get rid of it and initialize the filteredUsers
state to an empty array.
// src/App.jsx
import { useState, useEffect } from 'react'
// We no longer need the users variable so you can remove it from here
function App() {
// add this state
const [apiUsers, setApiUsers] = useState([])
const [searchItem, setSearchItem] = useState('')
// set the initial state of filteredUsers to an empty array
const [filteredUsers, setFilteredUsers] = useState([])
// fetch the users
useEffect(() => {
fetch('https://dummyjson.com/users')
.then(response => response.json())
// save the complete list of users to the new state
.then(data => setApiUsers(data.users))
// if there's an error we log it to the console
.catch(err => console.log(err))
}, [])
const handleInputChange = (e) => {
const searchTerm = e.target.value;
setSearchItem(searchTerm)
// filter the items using the apiUsers state
const filteredItems = apiUsers.filter((user) =>
user.firstName.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredUsers(filteredItems);
}
return (
// ... component rendering
)
}
This is all well and good but….. why can’t we see any items now when the component first loads? 😱 the answer is that we’re mapping over the filteredUsers
state to render the users, and it starts empty now, so no user is being shown until you start typing on the input. How can we render the full list of users when the component first loads and render the filtered users when we type on the input?
There’s a simple solution for that. Previously we initialized the filteredUsers
state with the complete list of users, we can’t do that now because the complete list of users is being stored in apiUsers
and that state starts as an empty array as well, but it gets updated when we fetch the users right? so all we need to do is to store the users in the apiUsers
state AND in the filteredUsers
state as well when fetching them from the API.
// src/App.jsx
import { useState, useEffect } from 'react'
function App() {
// ...states
useEffect(() => {
fetch('https://dummyjson.com/users')
.then(response => response.json())
.then(data => {
setApiUsers(data.users)
// update the filteredUsers state
setFilteredUsers(data.users)
})
.catch(err => console.log(err))
}, [])
// ...handler and component rendering
}
Awesome! that fixed the issue 🙌🏼 Now we have a fully functional item filtering functionality with data coming from an API 🎉
You can see that I also added a “No users found” message if no user matches with what we write in the input, you can do it by just editing the part where the users are being rendered, and make it like this:
// src/App.jsx
import { useState, useEffect } from 'react'
function App() {
// ...state, data fetching, handler
return (
<>
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
{filteredUsers.length === 0
? <p>No users found</p>
: <ul>
{filteredUsers.map(user => <li key={user.id}>{user.firstName}</li>)}
</ul>
}
</>
)
}
However you might notice that the message “No items found” flashes shortly when first rendering the page, we can fix that by adding a loading state, so that if the users are being fetched you can show a “Loading…” message or a spinner, we can also add an error
state to show a proper error message if the fetching doesn’t work.
// src/App.jsx
function App() {
const [apiUsers, setApiUsers] = useState([])
// initialize the loading state as true
const [loading, setLoading] = useState(true)
// initialize the error state as null
const [error, setError] = useState(null)
const [searchItem, setSearchItem] = useState('')
const [filteredUsers, setFilteredUsers] = useState([])
useEffect(() => {
fetch('https://dummyjson.com/users')
.then(response => response.json())
.then(data => {
setApiUsers(data.users)
setFilteredUsers(data.users)
})
.catch(err => {
console.log(err)
// update the error state
setError(err)
})
.finally(() => {
// wether we sucessfully get the users or not,
// we update the loading state
setLoading(false)
})
}, [])
//... on change handler
return (
<>
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
{/* if the data is loading, show a proper message */}
{loading && <p>Loading...</p>}
{/* if there's an error, show a proper message */}
{error && <p>There was an error loading the users</p>}
{/* if it finished loading, render the items */}
{!loading && !error && filteredUsers.length === 0
? <p>No users found</p>
: <ul>
{filteredUsers.map(user => <li key={user.id}>{user.firstName}</li>)}
</ul>
}
</>
)
}
Step 6: Refactoring a little (Bonus)
All is working well now, but what if we want to reuse the input and the list somewhere else? we could refactor this a little to make the components reusable, that way we can avoid repeating code. Let’s see step by step how we can do that.
ItemList Component
First create a new components
folder under src
. Let’s do the item list first, so create a new file called ItemList.jsx
under src/components
and copy the section that was rendering the items (just the mapping, without the loading
and error
state checking) to the newly created file. The items to be rendered as a list will now be gotten through props, so let’s change the filteredUsers
variable to items
to make it more general. The new component should look like this:
// src/components/ItemList.jsx
// get the items in the props
const ItemsList = ({items}) => {
return (
<>
{/* replace filteredUsers with items*/}
{items.length === 0
? <p>No users found</p>
: <ul>
{items.map(item => <li key={item.id}>{item.firstName}</li>)}
</ul>
}
</>
)
}
export default ItemsList
Excellent, now let’s use our new component in the App.jsx
file, replace the list of items being rendered with the new component and pass the filteredUsers
as the value for the items
prop.
// src/App.jsx
// ... other imports
import ItemList from './components/ItemsList'
function App() {
//... component logic
return (
<>
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
{loading && <p>Loading...</p>}
{error && <p>There was an error loading the users</p>}
{!loading && !error && <ItemList items={filteredUsers} />}
</>
)
}
Input Component
Let’s do the Input now, create a new file called Input.jsx
under src/components
, and copy the input tag from src/App.jsx
to the new component, which should now look like this:
// src/components/Input.jsx
const Input = () => {
return (
<input
type="text"
value={searchItem}
onChange={handleInputChange}
placeholder='Type to search'
/>
)
}
export default Input
Now, on this new component, we don’t have access to the searchItem
state and the handleInputChange
function so that will throw an error. We could pass those 2 values as props, but let’s do something more interesting. Let’s make it so that the input
element can manage its own value inside this component, so we create a new state which will handle that, and a new handler function to update the state.
You might be thinking how can we then filter the values on our App
component if it will no longer have access to the input value? Well the answer is: ¡With a callback! We can pass a callback function as a prop that will run inside the handler function and will take the input value as its argument. So change the Input.jsx
component to this:
// src/components/Input.jsx
import { useState } from "react"
const Input = ({ onChangeCallback }) => {
// state to handle the input value
const [value, setValue] = useState('')
// new handler function that will update the state
// when the input changes
const handleChange = (e) => {
const inputValue = e.target.value;
setValue(inputValue)
// if the component receives a callback, call it,
// and pass the input value as an argument
onChangeCallback && onChangeCallback(inputValue)
}
return (
<input
type="text"
value={value}
onChange={handleChange}
placeholder='Type to search'
/>
)
}
export default Input
And change the App.jsx
file to import our new Input
component and remove all the unnecessary code, remember to pass the previous handler function in App.jsx
as a prop to the Input component:
// src/App.jsx
import { useState, useEffect } from 'react'
// import the Input component
import Input from './components/Input'
function App() {
const [apiUsers, setApiUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
// we no longer need the searchItem state so you can remove it
const [filteredUsers, setFilteredUsers] = useState([])
// ... useEffect code without changes here
// this is the previous handleInputChange function, I changed
// its name to better represent its new functionality of only
// filtering the items
const filterItems = (searchTerm) => {
// we previously set the input state here,
// you can remove that now
const filteredItems = apiUsers.filter((user) =>
user.firstName.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredUsers(filteredItems);
}
return (
<>
{/* Use the new Input component instead of the input tag */}
<Input onChangeCallback={filterItems} />
{loading && <p>Loading...</p>}
{error && <p>There was an error loading the users</p>}
{!loading && !error && <ItemList items={filteredUsers} />}
</>
)
}
export default App
Perfect! The filtering should still be working as usual.
API call
We can also refactor our API call into a custom hook. Create a new hooks
folder under the src
folder and then create a new file there called useGetUsers.jsx
. Let’s now move our apiUsers
, loading
, and error
states, and the useEffect
where we’re making the API call to our new hook. We then return the users
, loading
, and error
states from our hook. We could also rename the apiUsers
state to just users
, since there are no other related variables in this new file.
// src/hooks/useGetUsers.jsx
import { useState, useEffect } from 'react'
export const useGetUsers = () => {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch('https://dummyjson.com/users')
.then(response => response.json())
.then(data => {
setUsers(data.users)
})
.catch(err => {
console.log(err)
setError(err)
})
.finally(() => {
setLoading(false)
})
}, [])
return { users, loading, error }
}
You can see that we also removed the setFiltered
state function from the useEffect
, so how can we set the filtered users now? We just need to make the following modification to our App.jsx
file.
// src/App.jsx
import { useState, useEffect } from 'react'
import Input from './components/Input'
import ItemList from './components/ItemsList'
// import our new hook
import { useGetUsers } from './hooks/useGetUsers'
function App() {
// use our custom hook to get our users and
// the error and loading variables
const {users, loading, error} = useGetUsers()
const [filteredUsers, setFilteredUsers] = useState([])
useEffect(() => {
// check if the users are not empty, if so then the
// API call was successful and we can update our
// filteredUsers state
if (Object.keys(users).length > 0) {
setFilteredUsers(users)
}
}, [users]) // this effect should run when the users state gets updated
const filterItems = (searchTerm) => {
// we now use 'users' instead of 'apiUsers' to do the filtering
const filteredItems = users.filter((user) =>
user.firstName.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredUsers(filteredItems);
}
// ... rest of the component stays the same
}
export default App
Conclusion
Woo! that was a lot to take in, right? 😅 but that’s it! You now know how to make a real time search filter in React with items coming from an API! 🤘🏼 And, if you went through the bonus step, you also learned some techniques on how to break your application into reusable components and custom hooks. Although, being honest, that refactoring is probably a bit of an overkill for such a small application, I did it just for the purposes of this article 😅
I hope you found this guide useful! And if there was something you didn’t understand or if you noticed something that can be improved, please feel free to mention it in the comments. Happy coding! 🤘🏼
Top comments (14)
One small observation on API call refactoring:
Here:
const filteredItems = apiUsers.filter((user) =>
user.firstName.toLowerCase().includes(searchTerm.toLowerCase())
);
you have to change apiUsers to users
You're right! thanks for the observation, I just made the change :)
Great Post. Thank you for making this super simple
Thank you! I'm glad it was helpful :)
Good👍
Thanks!
hey hi i need your help can you do this in RAWGraphs api call
I haven't worked with that API before, but you should be able to apply the same filtering technique with any API call that fetiches a list of data, just change the API url with the one you need.
To make it more robust i would add a debouce function to avoid super fast and repetitive api calls
Thanks for the suggestion! I actually thought about it, but I didn't see it necessary in this particular case since I'm getting the complete list of users with the API call when the component first renders, so there's only ever one API call.
I wanted to also add the case of making the API call when typing on the input and adding the debounce function there, but that would have made the article even longer, so I decided to leave it at that, maybe I'll do an article about using the debounce function in the future :)
Absolutely, in your example is no needed, by the way great example
Thank you!
Thanks for your dedication to help other in understanding such spectacular concepts.
Thanks for saving my time! I was stuck with this issue for like 4 hours. Thank you very much! 💖💖