Build a Simple Todo List

By this point we’re almost capable of building a simple todo list, so let’s give that a try. I encourage you to give it a shot on your own, but don’t spend too much time on it because there are a couple of gotchas.

Download the source code here and switch to the 04-start-todo-list branch.

The first thing we’ll want is a set of initial todo items. Outside of any functions, let’s create our initialTodos with an id field and a title:

import React, { useState } from "react";
import logo from "./logo.svg";
import "./App.css";

const initialTodos = [
  {
    id: 0,
    title: "Buy Radishes",
  },
  {
    id: 1,
    title: "Charge phone",
  },
];

Next, let’s setup in our App() function the form to add our todo Item. We’ll just focus on the ability to insert text into the field first. We’ll also setup our “todos” state:

function App() {
  const [textField, setTextField] = useState("");
  const [todos, setTodos] = useState(initialTodos);

  const handleNameFieldChange = (event) => {
    setTextField(event.target.value);
  };

  return (
    <div className="App">
      <h3>Todo List</h3>
      <form>
        <label>
          <input
            type="text"
            placeholder="Todo Item"
            value={textField}
            onChange={handleNameFieldChange}
            name="name"
          />
        </label>
        <input type="submit" value="Submit" />
      </form>
      <h3>Current Todos:</h3>
    </div>
  );

The above code is nothing new. We’re creating a controlled form that handles the form fields via state. Now let’s handle the trickier part of building our todo list. We’ll want to create a separate component for the individual todo item, and another component for the list. Then we pass in the todos items as props:

function TodoItem(props) {
  return <li>Todo Item: {props.title}</li>;
}

function TodoList(props) {
  let todos = [];
  props.todos.map((elem) => {
    todos.push(<TodoItem title={elem.title} />);
  });
  return <ul style={{ listStyleType: "none" }}>{todos}</ul>;
}

// Then way down at the end of our App() function's return() we'll render our TodoList component:

      </form>
      <h3>Current Todos:</h3>
      <TodoList todos={todos} />

To understand the code above, start at the todos state which is initially set to our initialtodos array. We take that array of todos and pass it into our <TodoList> component as the todos props.

Now we look inside our TodoList component and see that there are props passed into TodoList (props.todos). We loop through props.todos and create a <TodoItem> component for each todo item. Each of our todoItems is an object with the “title” and “id” properties, so we create a “title” and “key” prop with those properties. Key just helps React uniquely identify items in a list. (Like a social security number for todos).

Then we pass the list of TodoItem’s into a <ul> tag. Finally let’s look at TodoItem. It simply returns an <li> with the content of the todo. That’s our todo list!

Now we want to add new items to our todo list using that form field. For that we just need to update our state, and we already know how to do that. Give it a try yourself before looking at the below code:

(Hint: state is immutable. create a new list of todos with the new todo appended.) Following code belongs in App()

  const handleSubmit = (e) => {
    e.preventDefault();
    let newTodos = [...todos, { id: todos.length, title: textField }];
    setTodos(newTodos);
    setTextField("");
  };


  return (
    <div className="App">
      <h3>Todo List</h3>
      <form onSubmit={handleSubmit}>
        <label>

Because state is immutable, we want to create a newTodos array, destructure the existing elements into a new array, then add the new item to the end of the array in the same shape as the existing items. We’re working with static data so using the length of the array to get the id is fine for now.

Then we use our setTodos function to set the new state. Then clear the form field with setTextField(""); Finally we call the handleSubmit function on the submission of the form. Don’t forget to prevent the default page refresh with our e.preventDefault()

By now we should have a simple list that we can add items to:

Challenge time: Add the checkbox, and separate checked & unchecked items

It’s important that you spend time struggling through things, so give it a shot on your own first. Don’t worry about actually updating the items yet because there’s one part we haven’t covered yet required to update. But you CAN add the checkbox itself, you can set the state of each todo and see whether it’s currently checked or not, and you can separate your lists to look something like this:

Okay let’s setup our code to look like the above image. We need to

  1. add a state variable for each todo to determine if that todo is complete or not.
  2. add an input field for the checkbox on each todo.
  3. separate the incomplete and complete todos into separate lists

First up, let’s update the state variable for todos. Our initialTodos need a completed state:

const initialTodos = [
  {
    id: 0,
    title: "Buy Radishes",
    isCompleted: true,
  },
  {
    id: 1,
    title: "Charge phone",
    isCompleted: false,
  },
];

Now let’s immediately put the isCompleted state to use. We’ll create an input field on our todoItems:

function TodoItem(props) {
  return (
    <li>
      <input type="checkbox" checked={props.isCompleted} />
      Todo Item: {props.title}
    </li>
  );
}

That doesn’t quite work yet because we haven’t passed in the state isCompleted into our TodoItems component.

function TodoList(props) {
  let todos = [];
  props.todos.map((elem) => {
    todos.push(
      <TodoItem
        title={elem.title}
        isCompleted={elem.isCompleted}
        key={elem.id}
        id={elem.id}
      />
    );
  });

Now when we refresh we should be able to see our checkboxes checked or unchecked. We’ll also want to separate our todoLists. I just used an if conditional to push the todos to an incompleteTodos and a completeTodos array depending on the current state of that todo. Then I displayed those todos under the proper headings. The entire TodoList component looks like this:

function TodoList(props) {
  let incompleteTodos = [];
  let completeTodos = [];

  props.todos.map((elem) => {
    if (elem.isCompleted) {
      completeTodos.push(
        <TodoItem
          title={elem.title}
          isCompleted={elem.isCompleted}
          key={elem.id}
          id={elem.id}
        />
      );
    } else {
      incompleteTodos.push(
        <TodoItem
          title={elem.title}
          isCompleted={elem.isCompleted}
          key={elem.id}
          id={elem.id}
        />
      );
    }
  });
  return (
    <div>
      <h3>Incomplete Todos:</h3>
      <ul style={{ listStyleType: "none" }}>{incompleteTodos}</ul>
      <h3>Completed Todos:</h3>
      <ul style={{ listStyleType: "none" }}>{completeTodos}</ul>
    </div>
  );
}

Last thing, and this won’t change anything visually, but we probably want to make sure our newly created todos have an isCompleted state of false when we add them via our form, otherwise we might run into bugs.

    let newTodos = [
      ...todos,
      { id: todos.length, title: textField, isCompleted: false },
    ];

Updating Our Todos, Lifting State Up, & Prop Drilling

Now’s a good time to learn about “prop drilling” (aka “threading” or a few other terms) and “lifting state up”. Notice that we have 3 levels of component nesting in our code. App() at the outer most (not including index). Inside App() we have TodoList() and inside TodoList() we’re using TodoItem(). In React data only flows one way: Down. So if you have state that “relies” on multiple components, you’re supposed to “lift state up” to the highest component that needs that state.

Prop Drilling is where we “drill” or “thread” our data down the component hierarchy to where the data needs to be. Our TodoItems have the “state” of either complete or incomplete, but the state is already “lifted up” to the App() component. Because a user will click on the checkbox to complete or un-complete a task, and the checkbox markup is defined in the TodoItem, we need to “drill” the functionality from our App() component, through our TodoList, and into our TodoItem.

You can already see prop drilling in action in the above code. The completed state of each Todo is drilled from App() to TodoList() in this code:

 <TodoList todos={todos} />

Then “drilled” from TodoList to TodoItem here:

incompleteTodos.push(
    <TodoItem
      title={elem.title}
      isCompleted={elem.isCompleted}
      key={elem.id}
      id={elem.id}
    />
);

We can drill functions down through props also, which we’ll see in a moment. In our TodoItem component, let’s create an onChange handler to update the TodoItem every time someone clicks one of the checkboxes:

function TodoItem(props) {
  const updateTodo = () => {
    console.log("updated todo item with the ID of: ", props.id);
  };

  return (
    <li>
      <input
        type="checkbox"
        checked={props.isCompleted}
        onChange={updateTodo}
      />

This creates a function “updateTodo” and sticks it in the onChange handler to be run every time someone clicks a checkbox. Because each TodoItem is rendered as a separate component, the props.id of that todoItem is visible in the updateTodo without being passed in as a parameter. You should now be able to click on todo item check boxes and see the ID logged to the console.

But we want to update the Todo checked state, and that’s all the way up in App(). We need to write our updateTodo() function in App(), and pass in the Todo ID to be updated all the way from TodoItem. Just like we pass properties down via props, we can pass functions as well. So let’s create our updateTodo function:

// inside function App()
const updateTodo = (updatedId) => {
    console.log("updatedId in App() is: ", updatedId);
}

This function takes a single parameter when called. Now let’s pass it down to TodoItem.

    <h3>Current Todos:</h3>
    <TodoList updateTodo={updateTodo} todos={todos} />

Now that updateTodo is available within TodoList, we need to pass it to TodoItem:

// in function TodoList()
    <TodoItem
          title={elem.title}
          isCompleted={elem.isCompleted}
          updateTodo={props.updateTodo}
          key={elem.id}
          id={elem.id}
        />
      );
    } else {
      incompleteTodos.push(
        <TodoItem
          title={elem.title}
          isCompleted={elem.isCompleted}
          updateTodo={props.updateTodo}
          key={elem.id}
          id={elem.id}
        />

We just passed the updateTodo function all the way from App() to TodoItem(). Now let’s call updateTodo from inside TodoItem, and we’ll pass in the ID of the todo that was clicked on:

function TodoItem(props) {
  const updateTodo = () => {
    console.log("updated todo item with the ID of: ", props.id);
    props.updateTodo(props.id);
  };

Now when you click a checkbox the updateTodo function in App() gets called. From there we can update the state of the todo items. Because state is immutable we’ll want to create a new array, modify that array, and then set that array as the new state of Todos. Here’s the code that I came up with, but whatever works for you is fine! We’ll loop through each todo and check to see if its ID matches the ID of the todo the user clicked. If there’s a match, then take the isCompleted state of that todo and make it the opposite of what it was. Remember how we use our setTodos hook to update the state by passing in the new array of Todos.

const updateTodo = (updatedId) => {
    let newTodos = [...todos];
    newTodos.forEach((elem) => {
      if (elem.id == updatedId) {
        elem.isCompleted = !elem.isCompleted;
      }
    });
    setTodos(newTodos);
  };

Challenge Time: give user option to DELETE todos

Make a little delete icon/span/button next to each todo for the user to have the option to delete it. You know everything you need to complete this on your own so give it a shot before reading any further.

The first thing I’ll do is create a span of text that when clicked triggers a deleteTodo function. I’ll also create that deleteTodo function:

  const deleteTodo = () => {
    console.log("deleted Todo Id: ", props.id);
  }

  return (
    <li>
      <input
        type="checkbox"
        checked={props.isCompleted}
        onChange={updateTodo}
      />
      Todo Item: {props.title} <span onClick={deleteTodo}>Delete</span>
    </li>

The above code allows a user to trigger the deleteTodo function and gives us access to the ID of the todo we want to delete when a delete span is clicked. Now we’ll create the delete function in our App() component:

// in function App()
const deleteTodo = (deleteId) => {
    console.log("id of deletd todo: ", deleteId);
}

Now we need to pass this function to our TodoItem component (through our TodoList first):

// in return() of function App()
<h3>Current Todos:</h3>
<TodoList deleteTodo={deleteTodo} updateTodo={updateTodo} todos={todos} />

// in function TodoList()
    completeTodos.push(
        <TodoItem
          title={elem.title}
          isCompleted={elem.isCompleted}
          updateTodo={props.updateTodo}
          deleteTodo={props.deleteTodo}
          key={elem.id}
          id={elem.id}
        />
      );
    } else {
      incompleteTodos.push(
        <TodoItem
          title={elem.title}
          isCompleted={elem.isCompleted}
          updateTodo={props.updateTodo}
          deleteTodo={props.deleteTodo}
          key={elem.id}
          id={elem.id}
        />

Now we can call our App() functions deleteTodo() from our TodoItem() function:

const deleteTodo = () => {
    props.deleteTodo(props.id);
};

Finally we just need to filter the existing todos state to remove clicked Todo and set the state to the updated array:

const deleteTodo = (deletedId) => {
    let newTodos = todos.filter((elem) => elem.id !== deletedId);
    console.log(newTodos);
    setTodos(newTodos);
};

That’s it for this tutorial! For the completed code, check out the 04-done-todo-list branch from the repository.

Leave a Comment

Your email address will not be published. Required fields are marked *

want more details?

Fill in your details and we'll be in touch

%d bloggers like this: