How to create a Todo list app with React

Cover image

I've spent some time recently learning React. I've been working with Vue primarily for a couple of years, so it'll be interesting to see how the two frameworks compare. Everyone always praises Vue for it's shallow learning curve, and so far React has proved to be harder to learn. But I can also see that once you get past a certain level of familiarity, React would be a lot of fun to work with.

So, let's dive in!

The Todo list app

Everyone loves a todo list app! It's a great starting point for sizing up a new framework, as it requires such functionality as state management, two-way data binding, working with lists, styling, and controling form elements.

Our todo app should be able to:

  • add todo items
  • remove items
  • mark items as complete
  • remove items
  • edit items once the've been added
  • persist state to local storage

When it's complete it'll look like this:

This post won't go into detail on how to create every part of the application, as that would be a long post. Instead, I will touch on some of my key takeaways in learning React.

I've also taken the example app a step further by using Styled Components to handle styling, and create a light and dark mode theme. To dive deeper into the code, checkout the source code on Github.

To follow along with the example in this article, the easiest way is to create a new pen on Codepen.io

Creating components in React

You can create new components in React in two ways: Function components, or Class components.

Function component

function Title(props) {
  return <h1>{props.title}</h1>;
}

A function component is a simple function that takes a single argument, props (an object), and returns a React element, written in JSX.

ES6 Class component

class TodoItem extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <li>{this.props.todo}</li>
    );
  }
}

A class component uses a Javascript Class to extend React.Component. It requires a constructor function to initialize the object created by the class. The constructor takes an argument (props). Calling super(props) binds those props to the object, so that they can be accessed via this.

The class must also have a render() method that returns JSX.

I decided to concentrate on using class components. From what I've heard, using classes for components isn't currently the most popular way to create components. It was early on in React's lifetime, but has since been superceeded by using function components, and hooks. But if I'm going to learn React well, it's important not to skip the basics. The next app I build will probably use function components and hooks.

Using components

React isn't very prescriptive about file organisation. You can have multiple components in the same file, or split them out to have 1 per file with ES Modules. Coming from Vue's Single File Components, one component per file seems like the way to go for me.

If your component is in a separate file, make sure you export it. We export our title component referenced arlier with:

export Title;

The App.js file is our main application file. We import title with:

import Title from './Title'; // Relevant path directory

and then use it in our Applications JSX:

<Title title="React Todo App">

In our Title component, we are displaying the value of the title prop with {this.props.title}. Single curly braces in JSX allow you to evaluate an expression, similar to Vue's double curly braces, eg. {{ title }}.

One interesting difference from Vue to React is that props don't need to be defined when you declare a component. You can pass a prop with any name you like when using a components, and it will be available in the components props object. React does have default props and PropTypes which handle default props, prop typing and validation, similar to Vue's Prop declaration.

Understanding state in React

State is where data is stored in a web application. React has the concept of local state; each component can have it' own state (or data). Coming from Vue, think of it like Vue's data() method. To add state to our TodoItem class component, we set it in our constructor.

constructor(props) {
  super(props);

  this.state = { todo: this.props.todo };
}

We can update state with an input field, and a handler method attached to the keyUp event. We bind the value of the todo in state to the value of an input.

<label htmlFor="todo-item">
<input id="todo-item" onKeyUp={this.handleUpdate} value={this.state.todo}>

Events are handled in React elements in a similar way to handling events on DOM elements, with attributes. onKeyUp will call the handler method on each keyup.

We can add the handler method to the component by declaring it on the class:

  handleUpdate(e) {
    console.log(e.target.value); // Logs the updated value
  }

This new handler method we've created won't be bound to this just yet; you'll need to add:

this.handleUpdate = this.handleUpdate.bind(this);

in your constructor function. This part is a bit annoying, having to do this for every method on your class. It's not an issue specific to React, it's actually just the way that Classes work in Javascript. It can mean you end up having a lot of extra boilerplate to write when adding methods to your classes, and I'm sure it was one of the reasons the React community has moved towards function components.

Updating State with setState()

To update state, we use Reacts setState method. In our handleUpdate() method, we can update the value of our todo in state like this:

  handleUpdate(e) {
    this.setState(e.target.value)
  }

Note: it may be a good idea to use a debounce or throttling function here to limit how often state is updated. But that's outside the scope of this article.

With all that in place, our local state for the todo should now be in sync with the input field. React refers to inputs that are bound to their state like this as Controlled Components. It's a bit more setup than Vue's v-model, but again, maybe function components and hooks might allow this setup with less code.

Lifting up state

Having our props and state on a local TodoItem component is fine, but we're going to need to pass our state around from one component to another in the application. React reccommends lifting up state to the highest common ancestor component of all of other components that require access to that state.

So far we've created a <TodoItem> component that can take a prop, pass that prop value to local state, and use a text input to update state. We can create a new <TodoList> component that uses our <TodoItem> like so:

import TodoItem from './TodoItem';

class TodoList extends React.Component {
  constructor(props) {
    super(props);
  }

  handleUpdate(e) {
    this.setState(e.target.value)
  }

  render() {
    return (
      <ul>
        <TodoItem todo="Learn React" />
      </ul>
    );
  }
}

This will display our todo component with 'Learn React' as the value.

We can now 'lift up' our state to a common ancestor component by moving the state from <TodoItem> to <TodoList>.

import TodoItem from './TodoItem';

class TodoList extends React.Component {
  constructor(props) {
    super(props);

    this.state = { todo: '' };
    this.handleUpdate = this.handleUpdate.bind(this);  }

  handleUpdate(todo) {    this.setState({ todo });  }
  render() {
    return (
      <ul>
        <TodoItem todo={this.state.todo} updateTodo={this.handleUdpate} />      </ul>
    );
  }
}

Here we have moved the todo state into our <TodoList>. You can then remove it from the <TodoItem>. Once you've removed it, our state will no longer be in sync. The handleUpdate() method in <TodoItem> updates the local state. But now, we will want to make it update the state on <TodoList>. To do that, we can pass a function down as a prop to the <TodoItem> that will be invoked in the it's local handleUpdate() method.

Let's update <TodoItem> as follows:

class TodoItem extends React.Component {
  constructor(props) {
    super(props);

    this.handleUpdate = this.handleUpdate.bind(this);
  }

  handleUpdate(todo) {
    this.props.handleUpdate(todo);  }

  render() {
    return (
      <li>
        <label htmlFor="todo-item">
        <input id="todo-item" onKeyUp={this.handleUpdate} value={this.state.todo}>
      </li>
    );
  }
}

We remove the state from <TodoItem>, and replace the seState method with the handleUpdate method that we passed through as a prop. The state on our <TodoList> should now update when we update the todo item input.

Using React dev tools to view our components, state and props

React dev tools is an extenion for Chrome that allows you to look under the hood and see what's happening with your React application. Once installed, inspect your app (if using codepen, it's easiest to choose the 'Debug Mode' view to see your application on a window by itself).

You can now see the state on the TodoList updates as you type text into the Todo input.

A list of todo items

Now that we've learned how to create components, pass props down to them, and how to work with state, we have the foundations to extend our todo list example. Right now we just have one hard coded <TodoItem> component. We will want to turn our todo state into an array of data, and for each item in that array, create a <TodoItem>.

To understand how to work with lists and arrays of data in React, checkout the docs

For our example, lets hard-code some dummy data for now.

class TodoList extends React.Component {
  constructor(props) {
    super(props);

    this.state = { todos: [      'Learn React',      'Write blog posts',      'Kick back and relax'    ]};    this.handleUpdate = this.handleUpdate.bind(this);
  }

  handleUpdate(todo) {
    this.setState({ todo });
  }

  render() {
    return (
      <ul>        {this.state.todos.map((todo, index) => {          <TodoItem todo={todo} key={index} updateTodo={this.handleUdpate} />        })}      </ul>
    );
  }
}

Whoa! What's goin on there on line 20?

Inside curly braces in JSX, we can write anything that's a valid Javascript expression. So, we can use the Array.prototype.map() function to iterate over each item in our data, and return a <TodoItem> with the relevant data. Every item in a list in React requires a unique key so that React can apply reactivity to it's data. We can use the index of our mapped array to provide a simple index to the component.

You should now see a list of 3 todos. Let's stop and celebrate the progress we've made, and everything we've learned so far. Woohoo!

Next steps..

You've now learned a lot of React techniques, and you have enough of the foundations to complete our functionality in our todo app. To do that, we'll need to change our update method to update a single item in our data. We should turn our from strings to objects, adding a unique identifier to each in order to make them easier to identify in our state.

We could create a new component to 'add' new todo items. We could add a checkbox to 'complete' a single todo, and add a 'completed' property to a todo. After all, who doesn't love that feeling of ticking off an item from their todo list? We could add a button in our todo item to 'remove' an existing todo. You can imagine how each of those functions would use a method, passed down from the todo list component, that interacts with the todos array, either adding to or subtracting from it (using a unique identifier).

So go ahead and have fun extending your todo list example. Feel free to checkout the source code for tips, and be sure to read the React docs to get a better understanding of the concepts covered in this post!

I've had fun so far learning React. The next app I build, I'll dive into functional components and hooks a bit more, to start learning the advanced concepts. I'm hoping that writing components that way dries up some of code, as there is a fair bit of boilerplate and setup when writing class components. (Do I even have to call super in later versions of React?). It's also interesting to see different approaches to how things are done in Vue. For instance, passing functions down as props in Vue isn't really reccommended, as it goes against the 'Props down, Events up' mantra. But it seems that events are handled slightly differently in React, and passing functions down is an established pattern.

It'll be interesting to see what other differences from Vue I find along the way, and how they compare.

Thanks for reading, and happy coding!