Building a Real-Time Search Filter in React: A Step-by-Step Guide
Tags: webdev, javascript, beginners, react
Published: 2023-06-19 16:46:54

## Table of Contents
&nbsp;1. [Introduction](#introduction)
&nbsp;2. [Requirements](#requirements)
&nbsp;3. [Step 1: Setting Up the Project](#step-1-setting-up-the-project)
&nbsp;4. [Step 2: Creating the Input](#step-2-creating-the-input)
&nbsp;5. [Step 3: Rendering the list of items](#step-3-rendering-the-list-of-items)
&nbsp;6. [Step 4: Building the filtering functionality](#step-4-building-the-filtering-functionality)
&nbsp;7. [Step 5: Getting the items from an API and filtering them](#step-5-getting-the-items-from-an-api-and-filtering-them)
&nbsp;8. [Step 6: Refactoring a little (Bonus)](#step-6-refactoring-a-little-bonus)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 8.1. [ItemList Component](#itemlist-component)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 8.2. [Input Component](#input-component)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 8.3. [API call](#api-call)
&nbsp;9. [Conclusion](#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:

![Step1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s9g80c4a8z3813tq1u7n.png)

## 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:

```jsx
// 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 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9k3368mmfa2i7ppw9vn4.png)

## Step 3: Rendering the list of items

Next, let’s add a list of items and render it below the input.

```jsx
// 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 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ky7silycomg65judkcln.png)

## 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:

1. Add a state to save the filtered items, and set it to the users variable, 
2. 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
3. Render the filtered items instead of the users variable

So let’s get to it:

```jsx
// 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:

![Step4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jftrhiz1o9sna15nmmo0.gif)

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](https://dummyjson.com/docs/users) 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 :confused:. 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.

```jsx
// 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.

```jsx
// 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 🎉

![Step 5](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dx6doq3aqgumsd174vuf.gif)

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:

```jsx
// 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.

```jsx
// 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:

```jsx
// 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.

```jsx
// 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:

```jsx
// 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:

```jsx
// 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:

```jsx
// 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.

```jsx
// 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.

```jsx
// 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! 🤘🏼