DEV Community

Cover image for Let's build a search bar in React!
Tim Smith
Tim Smith

Posted on • Edited on

React Search Bar Let's build a search bar in React!

The original version of this article can be found here.

I know, I know...another task app...

Hear me out though! We are going to build a task app that also filters the list based on a search query in real time. Sound complicated? It's not as complicated as you may think, so let's dig in!

A quick note before we begin:
I'm going to be using Parcel as a bundler. It's pretty awesome and SUPER
easy to set up. I have another article about setting up a project with parcel
that will give you more information about the setup, so if I run through it too
fast here, I'd recommend checking that out here.

Set up our files

To get started, we will be creating our directory and entering it using the command line. To do this, open up your terminal and navigate to the directory in which you want to put your project. Once there, use the following line of code to create the directory for our project and enter it.

mkdir search-tasks && cd $_
Enter fullscreen mode Exit fullscreen mode

Now that we are in our project folder, we need to initialize our project with yarn or npm. I'll be using yarn for this project but the npm commands are pretty much the same.

yarn init -y
Enter fullscreen mode Exit fullscreen mode

We are going to just use the -y flag so it automatically configures things for us. We will go in and modify the package.json file soon.

Now that we have a package.json file, we should create our index.html and app.js files. You can use the line of code below in your terminal to create these two files at the same time.

touch index.html app.js
Enter fullscreen mode Exit fullscreen mode

Next we need to open our index.html file for editing and put the code below inside:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Search To-Do App</title>
</head>
<body>
  <div id="app"></div>
  <script src="./app.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Add packages to our project

Add packages to our project

Next we need to install the necessary packages to our project. In this case it's going to be React, React DOM, Parcel, Babel-Preset-env, Babel-Preset-React, and Bulma. To add these to our project, you can use NPM or Yarn. I will provide code for both, so you can choose whichever you are more comfortable with.

npm install react react-dom parcel babel-preset-env babel-preset-react bulma --save-dev

or

yarn add react react-dom parcel babel-preset-env babel-preset-react bulma
Enter fullscreen mode Exit fullscreen mode

What do these do?

NPM and Yarn are package managers that allow you to add prewritten code into your project. This can speed up development time astronomically. Below you'll find a quick description of what each of these packages do.

  • React: A library to speed up development (seems obvious for a React tutorial, right?) Link
  • React-DOM: A library which allows React to interact with the DOM in a browser.Link
  • Parcel: A bundling library which requires no config. Link
  • Babel-preset-env: A library which tells Parcel how to transform ES6 to work with many different browsers. Link
  • Babel-preset-react: A library which tells Parcel how to handle JSX. Link
  • Bulma: A CSS framework that uses flexbox and is easy to use. Link

Set up package.json and .babelrc

Before we can actually start building our React project, we need to add a .babelrc file to include the babel-presets we installed. First, create the file using the code:

touch .babelrc && open $_
Enter fullscreen mode Exit fullscreen mode

Once inside the file, we will add the following code to include the installed presets.

{
  "presets": ["env", "react"]
}
Enter fullscreen mode Exit fullscreen mode

Once we have our .babelrc file set up, we need to add the start scripts to the package.json file, so go ahead and open that. In the file, add the following code:

"scripts": {
    "start": "parcel index.html"
},
Enter fullscreen mode Exit fullscreen mode

Set up app.js file

Still with me? Great! The next step is to set up a component in our app.js file. We will be using state to manage our list, so we need to use a class component for this. First, let's import the necessary libraries to build our app.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import 'bulma/bulma';
Enter fullscreen mode Exit fullscreen mode

Then we can create an App component:

class App extends Component {
  render() {
    return(
      ...
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Then we need to make sure that our component is rendering to the DOM. We will use React DOM for this.

ReactDOM.render(<App />, document.getElementById('app'));
Enter fullscreen mode Exit fullscreen mode

Now we can add in our constructor and state. We will create a 'list' array in state. To start with, we will populate it with a few items so we can see our list:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      list: [
        "Go to the store",
        "Wash the dishes",
        "Learn some code"
      ]
    }
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

Awesome! Now that we have our list in the App component's state, lets display that list. I'm using Bulma for my styles, but you may be using something different. That's totally cool, you'll just need to adjust your classes accordingly.

class App extends Component {
  ...
  render() {
    return (
      <div className="content">
        <div className="container">
          <section className="section">
            <ul>
              {this.state.list.map(item => (
                <li key={item}>{item}</li>
              ))}
            </ul>
          </section>
        </div>
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

What is the code above doing?

We need to render our list. To do this, we are using a few Bulma classes to help give things a bit of room to breathe. The important part is happening with the <ul>. First we create the <ul> in which we want to display our list. Then we're going to escape the JSX by using curly braces and use a javascript function called .map(). We get the list we made in state with this.state.list and add .map() to the end of it. We then pass a callback function (in this case we're using an arrow function) to return the JSX we want to show.

A .map() function works similarly to a foreach because it loops through each item in the array. The argument we pass into the callback function (in this case item) will represent the item in each iteration of the loop. Inside of the return we will create an <li> and the text it will display will be item, or the text in the current index of our list array.

What do we get?

If we go back to our terminal and type in yarn start or npm run start, we can go to localhost:1234 in our browser to see the to-do list we made showing as an unordered list. Now lets allow users to add to-do items to the list.

Adding items to the list

This will be pretty simple. First we need to add the code to render an input box and a submit button. Our complete code for the rendered component should look like this for now:

<div className="content">
  <div className="container">
    <section className="section">
      <ul>
        {this.state.list.map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </section>
    <hr />
    <section className="section">
      <form className="form" id="addItemForm">
        <input
          type="text"
          className="input"
          id="addInput"
          placeholder="Something that needs ot be done..."
        />
        <button className="button is-info" onClick={this.addItem}>
          Add Item
        </button>
      </form>
    </section>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Add functionality to add item

Now that we have an input and button rendered, we need to make it do something. Otherwise our users won't be able to change the list at all. To do this, we need to add a function called addItem() to our component below the constructor but before the render method. We need this to run when we click our button. Upon click, it should take the text in the input and see if it's not empty. If it has text we will add it to the array in our state which will then update our rendered page. The following function will add the necessary functionality to our input:

addItem(e) {
    // Prevent button click from submitting form
    e.preventDefault();

    // Create variables for our list, the item to add, and our form
    let list = this.state.list;
    const newItem = document.getElementById("addInput");
    const form = document.getElementById("addItemForm");

    // If our input has a value
    if (newItem.value != "") {
      // Add the new item to the end of our list array
      list.push(newItem.value);
      // Then we use that to set the state for list
      this.setState({
        list: list
      });
      // Finally, we need to reset the form
      newItem.classList.remove("is-danger");
      form.reset();
    } else {
      // If the input doesn't have a value, make the border red since it's required
      newItem.classList.add("is-danger");
    }
  }
Enter fullscreen mode Exit fullscreen mode

We now have our function built but it doesn't know when to run or how to interperet the this keyword. We can tell react how to handle this with the following code in our constructor:

this.addItem = this.addItem.bind(this);
Enter fullscreen mode Exit fullscreen mode

And we can add an onClick trigger to our button, so our button should look like this:

<button className="button is-info" onClick={this.addItem}>
  Add Item
</button>
Enter fullscreen mode Exit fullscreen mode

We can test our application by using yarn start or npm run start and going to localhost:1234 in our browser. Our app now allows us to add an item to the list! Pretty cool!

Add a delete button

Adding a Delete button

Okay, so now our users can add items but what good does that do if they can't remove them once they're done? They'll just have items upon items upon items until the entropy peaks their anxiety levels and puts them in the grave early. Let's go ahead and save a few lives by adding a delete button, shall we?

Just like before, we'll be adding a function to handle this. The code below will allow our users to delete their list items when completed:

removeItem(item) {
    // Put our list into an array
    const list = this.state.list.slice();
    // Check to see if item passed in matches item in array
    list.some((el, i) => {
      if (el === item) {
        // If item matches, remove it from array
        list.splice(i, 1);
        return true;
      }
    });
    // Set state to list
    this.setState({
      list: list
    });
  }
Enter fullscreen mode Exit fullscreen mode

Add to constructor

We also need to add this function to the constructor. Just like before, we can do this like so:

this.removeItem = this.removeItem.bind(this);
Enter fullscreen mode Exit fullscreen mode

Add button to delete item

To make it easy for users to delete the item, we should add a delete button to the <li>. The code below will do that.

...
<ul>
  {this.state.list.map(item => (
    <li key={item}>
      {item} &nbsp;
      <span
        className="delete"
        onClick={() => this.removeItem(item)}
      />
    </li>
  ))}
</ul>
...
Enter fullscreen mode Exit fullscreen mode

Now we can run yarn start or npm run start in the terminal to view our changes. Now we can click on the x to delete that item from the list. Did it work?

Turning the list into a component

Whew! So far, so good.

Next we are going to turn our list into a component with it's own state and methods. I'm just going to create the component within our app.js file to keep things simple, but you could also create this component in a separate file and import it. Below the App component, create a class component called List with the following code:

class List extends React.Component {
    render() {
        return (
            <div>
            ...
            </div>
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

The code we want to render is just our list, so go back up to our App component and grab the following code to paste into the render function for our List component:

<ul>
  {this.state.list.map(item => (
    <li key={item}>
      {item} &nbsp;
      <span
        className="delete"
        onClick={() => this.removeItem(item)}
      />
    </li>
  ))}
</ul>
Enter fullscreen mode Exit fullscreen mode

Replace that code in the App component with a call to our List component like so:

<List items={this.state.list} delete={this.removeItem} />
Enter fullscreen mode Exit fullscreen mode

What's the code above doing?

Here, we are calling the List component and passing a few props in. The items prop is sending in the list we have stored in our state. The delete prop is passing in the removeItem method we created to delete the items.

Before this will work as expected, we need to modify our List component a bit. First we need to add the constructor so we can receive props.

class List extends React.Component {
    constructor(props) {
        super(props);
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

If we run the application with npm run start or yarn start, the application should look the same as it did before. We can still add items to our list with no problem. If we click the delete button...uh oh...it doesn't work. Why is this?

We don't have a method called removeItem within this component, so clicking the button doesn't call anything. Fortunately, we had the foresight to pass that method into this component as a prop. To regain the delete functionality, we can just alter the code for that button to the following:

<span className="delete" onClick={() => this.props.delete(item)} />
Enter fullscreen mode Exit fullscreen mode

So with a few adjustments, we now have a fully functioning list in a separate component. Now, onward to adding a search function.

Create a filtered item in List

The first part of adding a search bar will be to create an array of our filtered list.If the input bar is empty, it should display all items in the list. If there is text in the search bar, it should only show items that contain that text.

First, we'll add state to our List component and give it an array called filtered. The code below illustrates this.

class List extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            filtered: []
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Once we have a place to put our filtered list, we need to make sure the data is being put there.

Fair warning: This part may get a bit abstract, so do your best to stick with me. If I don't explain well enough or you're struggling with it, leave a comment and I'll try to help you get it.

Our original list of tasks is located in the App component, which in this case is the parent component.This state is being passed into the List component, in this case the child component, which gets re-rendered every time the task list is updated. What is the point of tell you this, you ask? We need to pass data into our filtered state every time the List component gets re-rendered. To do this, we will use a few lifecycle methods.

Lifecycle methods allow us to "hook" into a component at various points in it's render process. In this case, we're going to use componentDidMount and componentDidReceiveProps. componentDidMount will allow us to put the data into our filtered array when the component is initially rendered. On the other hand, componentDidReceiveProps will fire any time the props being passed into the component are changed.

To add these lifecycle methods to our List component, add the following code below the constructor but before the render function:

componentDidMount() {
  this.setState({
    filtered: this.props.items
  });
}

componentWillReceiveProps(nextProps) {
  this.setState({
    filtered: nextProps.items
  });
}
Enter fullscreen mode Exit fullscreen mode

Now if we change the .map() function we're using for our list to map over the filtered list instead of the items list being passed in through props, we should see the same thing on the front end.

Whoop-dee-doo

What's the big deal? The big deal is that now we have a list we can manipulate without altering the original list. All we have to do is modify our filter state and the items being displayed will also reflect that, but we haven't lost the original list by doing this.

Create the search bar itself

It seems to me that a good place to start with a search bar is...well...the search bar.Let's go ahead and create that. Inside of the div wrapper in our List component, lets add an input.

<div>
    <input type="text" className="input" placeholder="Search..." />
    <ul>
    ...
    </ul>
</div>
Enter fullscreen mode Exit fullscreen mode

Cool! Now we have a search bar. If only it actually worked...

Make the search bar search

We have a nice looking search bar, but it doesn't really do anything other than look pretty. Maybe this is good enough, but I think there is more to life than just being really, really, ridiculously goodlooking. Let's add the "brains".

Really Really Ridiculously Good Looking

To start, we'll add a method called handleChange after our lifecycle methods. We will be passing in e as an argument which will stand for event. Inside of the method, we'll create two variables which will hold the original task list being passed in as props as well as the filtered list before it's passed into state.

We also need to add an if statement so that the .filter() function only runs if the input isn't empty. Otherwise and empty search bar won't display any tasks. So if the search bar is not empty, we want to run the .filter() function and see if the current item contains the search terms. If it does, then we will return that item to the newList array.

handleChange(e) {
        // Variable to hold the original version of the list
    let currentList = [];
        // Variable to hold the filtered list before putting into state
    let newList = [];

        // If the search bar isn't empty
    if (e.target.value !== "") {
            // Assign the original list to currentList
      currentList = this.props.items;

            // Use .filter() to determine which items should be displayed
            // based on the search terms
      newList = currentList.filter(item => {
                // change current item to lowercase
        const lc = item.toLowerCase();
                // change search term to lowercase
        const filter = e.target.value.toLowerCase();
                // check to see if the current list item includes the search term
                // If it does, it will be added to newList. Using lowercase eliminates
                // issues with capitalization in search terms and search content
        return lc.includes(filter);
      });
    } else {
            // If the search bar is empty, set newList to original task list
      newList = this.props.items;
    }
        // Set the filtered state based on what our rules added to newList
    this.setState({
      filtered: newList
    });
  }
Enter fullscreen mode Exit fullscreen mode

When using .contains(), it is case sensitive. This could make searching within our tasks more difficult for the user because they will have to know exactly what casing the item uses. To get around this, we can just change everything lowercase in our function so we know what case everything will be using when we do our search. The comments above also explain this.

Adding the method to the input

We're so close! Before we can use the handleChange() method, we need to bind the this keyword to it. Inside of our constructor, after the state, add the following code to bind our this keyword for the method.

this.handleChange = this.handleChange.bind(this);
Enter fullscreen mode Exit fullscreen mode

Finally, we can add an event handler to the input item to call the method whenever the content is changed.This last piece will be what actually makes the search function work. Add onChange={this.handleChange} to the input element to make it look like this:

<input type="text" className="input" onChange={this.handleChange} placeholder="Search..." />
Enter fullscreen mode Exit fullscreen mode

Conclusion

Running the application should now allow you to create, delete, and search tasks.There's a lot of text here, but it's not actually THAT complicated.

Was this helpful for you? If you run into any problems, let me know and I will get this tutorial updated. I have also added the codepen with the full code below so you can play around with it or compare code.

The original version of this article can be found here.

Top comments (27)

Collapse
 
zrbecker profile image
Zachary Becker

Thanks for the article.

You addItem/removeItem has a fairly unlikely race condition in it.

You are reading the list from the state, modifying it, then using this.setState. However, this.setState does not update the state immediately.

In theory, addItem could get called twice in quick succession, and you would lose one of the items added. For example with this possible ordering of events:

  • Initial List ["a", "b", "c"]
  • addItem Called with "d", modified list ["a", "b", "c", "d"], setState called
  • addItem called with "e", modified list ["a", "b", "c", "e"], setState called
  • State is updated with ["a", "b", "c", "d"]
  • State is updated with ["a", "b", "c", "e"]

Just something to keep in mind.

Collapse
 
iam_timsmith profile image
Tim Smith

Interesting. How would you suggest preventing that issue in a project like this?

Collapse
 
zrbecker profile image
Zachary Becker • Edited

Use call back format for setState.

this.setState(state => {
  const list = state.list;
  /* modify list */;
  return {
    list: modifiedList
  };
});
Enter fullscreen mode Exit fullscreen mode
Collapse
 
davidasync profile image
David Async • Edited

Great tutorial, but why not use facebook cra instead ?

Collapse
 
dance2die profile image
Sung M. Kim

If cra is your choice, you can try c-r-a-p 💩-create-react-app-parcel 😃

Collapse
 
iam_timsmith profile image
Tim Smith

I have not heard of this. It looks cool though. Thanks for the link!

Collapse
 
iam_timsmith profile image
Tim Smith

It’s really personal preference, but to be honest I haven’t used create-react-app in a while and forgot it existed.

Collapse
 
migueloop profile image
Miguel Ruiz

At the end It's better to make it your own way. create-react-app is for fast dev or examples but not for real projects.

Thread Thread
 
jason_espin profile image
Jason Espin

That's not necessarily true. I know plenty of enterprit level projects that use React that were created with CRA. As long as you configure and secure it, it's the best solution for making a start on a React project.

Thread Thread
 
migueloop profile image
Miguel Ruiz

I recommend you to read this article:

medium.com/@francesco.agnoletto/i-...

Thread Thread
 
jason_espin profile image
Jason Espin

I couldn't disagree with this more. In a majority of cases you don't need any additional configuration.

Collapse
 
machy44 profile image
machy44 • Edited

Nice Article.

When I use getDerivedStateFromProps() in same way you use componentWillReceiveProps() in List component searching doesn't work.

static getDerivedStateFromProps(nextProps) {
return {
filtered: nextProps.items
};
}

I didn't know that getDerivedStateFromProps is called every time we setState(). Link for lifecycles projects.wojtekmaj.pl/react-lifecy....

I wanted to share this thought with you cause I didn't give enough attention about differences between componentWillReceiveProps and getDerivedStateFromProps and IMHO this is kind of important.

Collapse
 
iam_timsmith profile image
Tim Smith

This is awesome! Thanks for the link!

Collapse
 
machy44 profile image
machy44

Np. Thank you for the article

Collapse
 
nemethricsi profile image
Richard • Edited

Hi there,

Really liked your tutorial Tim! <3 I thought I try all of that with functional components and this is what I got (feel free to correct me - I'm kinda rookie in webdev). Seems working though!

Minor changes I made:

  • used functional components instead of class components. Therefore used React.useState() for storing variables in the state.

  • input element for adding new todo is a controlled component and handled also in the state. Found this approach more "react-ish".

  • for deleting a todo I also used filter() method - seems more handy for me.

  • some naming changes

App.js

// App.js

import React, { useState, Fragment } from "react";
import List from "./List";

const App = () => {
  const [userInput, setUserInput] = useState("");
  const [list, setList] = useState([
    "walk the dog",
    "buy the milk",
    "learn some code"
  ]);

  // userinput is controlled by the App component
  const handleChange = e => {
    setUserInput(e.target.value);
  };

  const addItem = e => {
    if (userInput !== "") {
      setList([...list, userInput]);
      setUserInput("");
    }
  };

  const removeItem = item => {
    const updatedList = list.filter(listItem => listItem !== item);
    setList(updatedList);
  };

  return (
    <Fragment>
      <List list={list} removeItem={removeItem} />
      <hr />
      <form>
        <input
          placeholder="Something that needs to be done..."
          value={userInput}
          onChange={handleChange}
        />
        <button type="button" onClick={addItem}>
          {'Add Item'}
        </button>
      </form>
    </Fragment>
  );
}

export default App;

List component:

// List.js

import React, { useState, useEffect, Fragment } from "react";

const List = ({ list, removeItem }) => {
  const [filtered, setFiltered] = useState([]);

  useEffect(() => {
    setFiltered(list);
  }, [list]);

  const handleChange = e => {
    let currentList = [];
    let newList = [];

    if (e.target.value !== "") {
      currentList = list;
      newList = currentList.filter(item => {
        const lc = item.toLowerCase();
        const filter = e.target.value.toLowerCase();
        return lc.includes(filter);
      });
    } else {
      newList = list;
    }
    setFiltered(newList);
  };

  return (
    <Fragment>
      <input 
        type="text" 
        placeholder="Search..." 
        onChange={handleChange} 
      />
      <ul>
        {filtered.map((todo, i) => (
          <li key={`${todo}-${i}`}>
            {todo} &nbsp;
            <span onClick={() => removeItem(todo)}>x</span>
          </li>
        ))}
      </ul>
    </Fragment>
  );
};

export default List;

default create-react-app index.js...

// index.js

import React from "react";
import ReactDOM from "react-dom";

import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  rootElement
);
Collapse
 
skillitzimberg profile image
Scott Bergler

"Fair warning: This part may get a bit abstract, so do your best to stick with me. If I don't explain well enough or you're struggling with it, leave a comment and I'll try to help you get it." ~ T. Smith

I love the generous spirit and the acknowledgement that sometimes what we think we're say/writing is not what is heard/understood.

Collapse
 
moopet profile image
Ben Sinclair

There's a typo: mkdir search-tasks && $_ should be mkdir search-tasks && cd $_

Collapse
 
iam_timsmith profile image
Tim Smith

Fixed. Thank you!

Collapse
 
victortolbert profile image
Victor Tolbert

Thanks for the article.

FYI, I don't see the instructions to modify the package.json to include the start script.

Collapse
 
iam_timsmith profile image
Tim Smith

Thanks! I have added the piece about scripts in the package.json file.

Collapse
 
sagar profile image
Sagar

Hi Tim,

I found typo error at Next we need to install the necessary packages to our package. => Next we need to install the necessary packages to our project

Collapse
 
iam_timsmith profile image
Tim Smith

Corrected. Good catch. It sounded like I was trying to be xhibit or something.

Collapse
 
maynardhartman profile image
Maynard J. Hartman, Jr.

Great Article Thanks...

Collapse
 
iam_timsmith profile image
Tim Smith

No problem. I’m glad it was helpful!

Collapse
 
anascann profile image
Anas Khan

Hey! nice article. but how do we implement search via using two keywords. like i have rows of some information. i want to search using keywords age and gender. any idea?