How To Build a React To-Do App with React Hooks

PostedDecember 12, 2019 8.5k views JavaScriptReact

While this tutorial has content that we believe is of great benefit to our community, we have not yet tested or edited it to ensure you have an error-free learning experience. It's on our list, and we're working on it! You can help us out by using the "report an issue" button at the bottom of the tutorial.

Introduction

React is a front-end JavaScript library that can be used to create interactive user interfaces for your application. In this tutorial, you will create a to-do app that covers all four aspects of a CRUD: Create, Read, Update, and Delete.

This type of project is often done with Class components, but this application will instead integrate React Hooks. React Hooks allow for functional components to have a state and utilize lifecycle methods, allowing you to avoid using class components and have more modular and readable code.

You can check out the completed project at the CodeSandbox for this project.

Step 1 — Starting a React App

Navigate to the place you would like your new application to be located and type:

  • npx create-react-app react-to-do

Note: Running npx before the command allows it to be installed if it is not already installed globally on your machine.

Once it is done, navigate into the project:

  • cd react-to-do

Then run the project:

  • npm run start

Navigate to http://localhost:3000/ in your browser to see the spinning React logo.

At the time of this writing, React Hooks are in React 16.7, but React 16.7 is in alpha. You’ll need to install that specifically until it comes out of alpha:

  • npm install -S react@16.7.0-alpha.2 react-dom@16.7.0-alpha.2

Step 2 — Styling Your Application

Move into your src/App.css file and add in the three classes we will be using throughout our app. Styling won’t be the focus of this tutorial, so we’ll keep this section short.

src/App.css
.app {
  background: #209cee;
  padding: 30px;
  height: 100vh;
}

.todo-list {
  background: #e8e8e8;
  border-radius: 4px;
  padding: 5px;
  max-width: 400px;
}

.todo {
  background: #fff;
  box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.15);
  padding: 3px 10px;
  font-size: 12px;
  margin-bottom: 6px;
  border-radius: 3px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

Step 3 — Reading a List of Items

With your application running and the styling ready to be used, let’s start on the Read part of CRUD. We’ll want to make a list of things so that we can Read/view the list.

Adding in State

Go into your src/App.js file and add a state to our component. We are going to be using React Hooks, so state will look a little different than if we used classes.

src/App.js
function App() {
  const [todos, setTodos] = useState([
    { text: "Learn about React" },
    { text: "Meet friend for lunch" },
    { text: "Build really cool todo app" }
  ]);

  // we'll render our todos here ...
  // return <div></div>
}

The component is a functional component. In past versions of React, functional components were unable to handle state, but now, by using Hooks, they can.

  • The first parameter, todos, is what we are going to name our state.
  • The second parameter, setTodos, is what we are going to use to set the state.

The hook of useState is what React uses to hook into the state or lifecycle of the component. We then create an array of objects and we have the beginnings of our state.

Comparing to a Class Component

Let’s take a moment to see how this would’ve been done with classes:

class App extends Component {
  state = {
    todos: [
      { text: "Learn about React" },
      { text: "Meet friend for lunch" },
      { text: "Build really cool todo app" }
    ]
  }

  setTodos = todos => this.setState({ todos });

  render() {
    return <div></div>
  }

}

React Hooks allows for much cleaner code.

Our Todo Component

We will want to create a component that we can use later on in the return of the main App component. We will call that Todo and it will pass in the todo and show the “text” part of the todo (todo.text), like so:

const Todo = ({ todo }) => <div className="todo">{todo.text}</div>;

Let’s see how we will use that in our App component.

Using the Todo Variable to Get a List Returned

Go down to the return part of the App component and remove almost everything. We want to empty out that return part so that the spinning logo we saw earlier when we navigated to http://localhost:3000 goes away. Instead, our list will be displayed on the page.

By using the JavaScript method, map(), we are able to create a new array of items by mapping over the todo items from state and displaying them by index.

Let’s create a list of items:

src/App.js
  return (
    <div className="app">
      <div className="todo-list">
        {todos.map((todo, index) => (
          <Todo
            key={index}
            index={index}
            todo={todo}
          />
        ))}
      </div>
    </div>
  );

Navigate to your browser where React is running. You will see something like this:

To-do app view of items

Step 4 — Creating New Items in the To-Do List

Now, let’s give our application the power to create a new item for our to-do app.

While in the src/App.js file, we need to add a couple of things. At the top, we will add another component called TodoForm. In this component we want to:

  • Start with an empty state for an input field.
  • Be able to update the form by setting the state.
  • Handle the submit.

Setting our Empty State for the Form Input

To set our state, we will write it like so:

const [value, setValue] = useState("");

The first is the “value” and the second is how we are going to be setting the state. The state starts off empty, and as we add things to our state, it will add it to our list of to-do items.

We will want to add in a handleSubmit variable that can handle our addTodo function (we will make that function soon) and add the item to the list. If nothing is in the input box and the user presses ENTER, we want it to not do anything (that is, not add in an empty tile to the list).

Adding that functionality into a form that has an input box, we will have our code look like this:

[list src/App.js]
function TodoForm({ addTodo }) {
  const [value, setValue] = useState("");

  const handleSubmit = e => {
    e.preventDefault();
    if (!value) return;
    addTodo(value);
    setValue("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        className="input"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
    </form>
  );
}

Let’s go ahead and build the addTodo function now. Staying within App.js, under the state of the App component, the function will be able to grab the existing list of items, add on the new item, and display that new list.

src/App.js
const addTodo = text => {
    const newTodos = [...todos, { text }];
    setTodos(newTodos);
  };

Notice that there is no this.state. With the new React Hooks, we have no more need to use that, since the new Hooks understand that this.state is going to be implied in certain places.

There is a spread operator in the code as well. The three dots before the todos copy the list for us so that we are able to add on the new to-do item. Then using our keyword that we set earlier, we will set the state with setTodos.

By using the TodoForm down in the return of the App component, we will see that input box pop up now. The entire src/App.js file will look like this so far:

src/App.js
import React, { useState } from "react";
import "./App.css";

const Todo = ({ todo }) => <div className="todo">{todo.text}</div>;

function TodoForm({ addTodo }) {
  const [value, setValue] = useState("");

  const handleSubmit = e => {
    e.preventDefault();
    if (!value) return;
    addTodo(value);
    setValue("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        className="input"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
    </form>
  );
}

function App() {
  const [todos, setTodos] = useState([
    { text: "Learn about React" },
    { text: "Meet friend for lunch" },
    { text: "Build really cool todo app" }
  ]);

  const addTodo = text => {
    const newTodos = [...todos, { text }];
    setTodos(newTodos);
  };

  return (
    <div className="app">
      <div className="todo-list">
        {todos.map((todo, index) => (
          <Todo
            key={index}
            index={index}
            todo={todo}
          />
        ))}
        <TodoForm addTodo={addTodo} />
      </div>
    </div>
  );
}
  export default App;

You can now add in a to-do item to your list in your browser.

Create function in the to-do app

Step 5 — Updating Items in the To-Do List

Let’s add the functionality to cross off an item on our To-Do list.

Updating Our State

Our state in our App component needs a little extra for the “Completed” status to be able to change. We will be adding in another key/value pair to our list of objects. By adding in an “isCompleted: false” value, we will set that to false to begin with and will, when prompted, change that to true.

src/App.js
const [todos, setTodos] = useState([
     {
       text: "Learn about React",
       isCompleted: false
     },
     {
       text: "Meet friend for lunch",
       isCompleted: false
     },
     {
       text: "Build really cool todo app",
       isCompleted: false
     }
   ]);

We will need a function like the addTodo function, but this one will be able to “complete” an item. We will want to do some similar things that we did in addTodo, like using the spread operator to grab the current list of items. In this function, we will be changing the isCompleted status to true so that it knows it has been completed. It will then update the state and set the state to the newTodos.

src/App.js
const completeTodo = index => {
     const newTodos = [...todos];
     newTodos[index].isCompleted = true;
     setTodos(newTodos);
   };

By using completeTodo in the Todo function, we are going to be able to use that functionality. When the Complete button is clicked, it will add in the textDecoration styling and cross-out the item. We are using a ternary operator, a feature within ES6 JavaScript, which is another way of doing an if/else statement. This is our way of completing an item on the list and “updating” the list. The code will look as follows:

src/App.js
function Todo({ todo, index, completeTodo }) {
  return (
    <div
      className="todo"
      style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
    >
      {todo.text}

      <div>
        <button onClick={() => completeTodo(index)}>Complete</button>
      </div>
    </div>
  );
}

Go down to the return of the App component and add in the following line:

completeTodo={completeTodo}

The code will now look like this:

src/App.js
<div className="card-content">
    {todos.map((todo, index) => (
        <Todo
            key={index}
            index={index}
            todo={todo}
            completeTodo={completeTodo}
        />
    ))}
</div>

Returning to the browser, your to-do app will look like this when a “Complete” button is clicked.

To-do app with item crossed out

Now we can read our list, add to our list, and update the completed status of each item. Next, we will add delete functionality.

Step 6 — Deleting a To-Do Item

By adding just a couple lines, we will be able to add in the functionality of deleting an item.

We will go ahead and build the removeTodo function so that when we click on an “X” to delete an item, the item will be deleted. That function will be located by the others underneath the state of the App component.

In this removeTodo function, we will again be using the spread operator, but once we grab that current list, we will be splicing the chosen index off of the array of items. Once that is removed, we will return the new state by setting it with setTodos to be newTodos.

src/App.js
const removeTodo = index => {
     const newTodos = [...todos];
     newTodos.splice(index, 1);
     setTodos(newTodos);
   };

In your Todo function, you will want to add in this line:

<button onClick={() => removeTodo(index)}>x</button>

This will look like the following:

src/App.js
function Todo({ todo, index, completeTodo, removeTodo }) {
  return (
    <div
      className="todo"
      style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
    >
      {todo.text}

      <div>
        <button onClick={() => completeTodo(index)}>Complete</button>
        <button onClick={() => removeTodo(index)}>x</button>
      </div>
    </div>
  );
}

You’ll also see that we are bringing in removeTodo at the top and then using it in the onClick of the “X”.

Adding in removeTodo in the Todo part of returning the App component, our “delete” will be fully functional. Add it in here:

src/App.js
<Todo
    key={index}
    index={index}
    todo={todo}
    completeTodo={completeTodo}
    removeTodo={removeTodo}
/>

With that added in, go to your browser and you’ll see a button with an “X” that, when clicked, deletes the item completely.

To-do app with delete button

Step 7 — Bringing the App Together

The entire src/App.js file will look like this in the end:

src/App.js
import React, { useState } from "react";
import "./App.css";

function Todo({ todo, index, completeTodo, removeTodo }) {
  return (
    <div
      className="todo"
      style={{ textDecoration: todo.isCompleted ? "line-through" : "" }}
    >
      {todo.text}

      <div>
        <button onClick={() => completeTodo(index)}>Complete</button>
        <button onClick={() => removeTodo(index)}>x</button>
      </div>
    </div>
  );
}

function TodoForm({ addTodo }) {
  const [value, setValue] = useState("");

  const handleSubmit = e => {
    e.preventDefault();
    if (!value) return;
    addTodo(value);
    setValue("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        className="input"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
    </form>
  );
}

function App() {
  const [todos, setTodos] = useState([
    {
      text: "Learn about React",
      isCompleted: false
    },
    {
      text: "Meet friend for lunch",
      isCompleted: false
    },
    {
      text: "Build really cool todo app",
      isCompleted: false
    }
  ]);

  const addTodo = text => {
    const newTodos = [...todos, { text }];
    setTodos(newTodos);
  };

  const completeTodo = index => {
    const newTodos = [...todos];
    newTodos[index].isCompleted = true;
    setTodos(newTodos);
  };

  const removeTodo = index => {
    const newTodos = [...todos];
    newTodos.splice(index, 1);
    setTodos(newTodos);
  };

  return (
    <div className="app">
      <div className="todo-list">
        {todos.map((todo, index) => (
          <Todo
            key={index}
            index={index}
            todo={todo}
            completeTodo={completeTodo}
            removeTodo={removeTodo}
          />
        ))}
        <TodoForm addTodo={addTodo} />
      </div>
    </div>
  );
}

export default App;

Conclusion

A to-do app can be a great reminder or starting point when it comes to CRUD in web development. Being able to read information, create new information, update existing information, and delete information can be powerful in any application.

In this tutorial, your created a CRUD To-do list app with React Hooks, which allowed for a more straight-forward way of coding and made the code clear and concise. If you’d like to learn more about React, check out the official React docs.

0 Comments

Creative Commons License