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 $_
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
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
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>
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
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 $_
Once inside the file, we will add the following code to include the installed presets.
{
"presets": ["env", "react"]
}
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"
},
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';
Then we can create an App component:
class App extends Component {
render() {
return(
...
)
}
}
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'));
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"
]
}
}
...
}
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>
)
}
}
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>
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");
}
}
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);
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>
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!
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
});
}
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);
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}
<span
className="delete"
onClick={() => this.removeItem(item)}
/>
</li>
))}
</ul>
...
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>
)
}
}
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}
<span
className="delete"
onClick={() => this.removeItem(item)}
/>
</li>
))}
</ul>
Replace that code in the App component with a call to our List component like so:
<List items={this.state.list} delete={this.removeItem} />
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);
}
...
}
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)} />
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: []
}
}
}
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
});
}
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.
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>
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".
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
});
}
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);
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..." />
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)
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 usingthis.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:addItem
Called with "d", modified list["a", "b", "c", "d"]
,setState
calledaddItem
called with "e", modified list["a", "b", "c", "e"]
,setState
called["a", "b", "c", "d"]
["a", "b", "c", "e"]
Just something to keep in mind.
Interesting. How would you suggest preventing that issue in a project like this?
Use call back format for setState.
Great tutorial, but why not use facebook cra instead ?
If cra is your choice, you can try c-r-a-p π©-create-react-app-parcel π
I have not heard of this. It looks cool though. Thanks for the link!
Itβs really personal preference, but to be honest I havenβt used create-react-app in a while and forgot it existed.
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.
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.
I recommend you to read this article:
medium.com/@francesco.agnoletto/i-...
I couldn't disagree with this more. In a majority of cases you don't need any additional configuration.
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.
This is awesome! Thanks for the link!
Np. Thank you for the article
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
List component:
default
create-react-app
index.js..."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.
There's a typo:
mkdir search-tasks && $_
should bemkdir search-tasks && cd $_
Fixed. Thank you!
Thanks for the article.
FYI, I don't see the instructions to modify the
package.json
to include thestart
script.Thanks! I have added the piece about scripts in the package.json file.
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
Corrected. Good catch. It sounded like I was trying to be xhibit or something.
Great Article Thanks...
No problem. Iβm glad it was helpful!
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?