Create Todo App With React (Incl React Hooks)

Apr 4, 2019 | 15 min read

React React Hooks

Featured Image

Photo by Anete > Lūsiņa on Unsplash

💻 Checkout The Demo - Click Here
📜 Source Code - Click Here

Most of the react development courses for beginners starts with building a todo app 😄 Because it has simple logics yet helps to learn 4 major operations. Some call them CRUD (Create, Read, Update and Delete).

You may have created todo apps earlier with React, but in this tutorial, I will be utilizing React Hooks which was introduced recently to manage state inside functional components.

Generating a React App

There are many ways that you can create a React starter project. I often use yarn when dealing with dependencies. To get a starter project run

$ yarn create react-app todo-app

then you will be getting a file structure like below 😍

Start file structure

I have deleted logo.svg App.test.js serviceWorker.js and index.css as I don't use them in these projects. For general styles, I will use App.css. When you remove them don't forget to remove all imports of them from particular files. If not errors will be shown 😃

Installing Initial Dependencies

I hope to make this little app with antd which is a UI framework, I love it's simple design and offers a lot of features. Most importantly it's really easy to work with Ant Design. Therefore I'm gonna first install antd to my project :

$ yarn add antd

before you start working with antd, there is one important thing that you should do, that is importing ants's CSS file into index.css file. I will be using Overpass google font with this project, so at the same time, I'm gonna import that also. After importing it looks like below

index.css
@import "~antd/dist/antd.css";
@import url("https://fonts.googleapis.com/css?family=Overpass:300,400,700&display=swap");

* {
  box-sizing: border-box;
  margin: 0;
}

body {
  font-family: "Overpass", sans-serif;
}

Okay! For now enough importing 😄 Let's start building our to-do App! First, I'm gonna use the Card component of antd and make a square in the middle of our browser screen. Square in the sense a wrapper component to hold our inner components.

App.js
import React, { Component } from "react"
import { Card } from "antd"

class App extends Component {
  render() {
    return <Card title="My Todos">My Todo List</Card>
  }
}

export default App

After that, I'm going to apply some changes to the Card element with some custom CSS. There is a way that you can use CSS with React App that is built with create-react-app that is CSS modules. What I will do is create a file named app.module.css and import it inside our App.js. In my app.module.css file, I will include the following

app.module.css
.app {
  max-width: 500px;
  margin: 50px auto !important;
}

@media screen and (max-width: 980px) {
  .app {
    width: 90% !important;
    margin: 20px auto !important;
  }
}
App.js
import React, { Component } from "react"
import { Card } from "antd"

//styles
import styles from "./app.module.css"

class App extends Component {
  render() {
    return (
      <Card className={styles.app} title="My Todos">
        My Todo List
      </Card>
    )
  }
}

export default App

By this method, you will be able to write modular css that are very specific to a component and those styles won't affect other modules that have the same class. Suppose if you don't use this method and uses the regular method, which is just writing CSS in a global file, you won't be able to use same class name twice in the app, because you write CSS in a global file that affects the whole project and those CSS rules will apply to all components that have the same CSS name.

After importing you can refer to your styles with dot notation like styles.app , that's why the className for the Card component is styles.app and it generates a unique class for that component.

Unique Class Name for a component

And when you add it and save the file, in the browser it must display like below. Starter look

Now we have our base component created inside our App.js file. We mainly need 2 more components,

  1. A form to add todos
  2. A todo item with action buttons (to delete, update and make as complete)

Creating TodoForm Element

Before creating out first separated React element, What I'm gonna do is create a folder called components inside src folder, I will be storing all my components inside it. That way it's easy to manage components rather than creating all of them inside App.js file.

I will name my form component as TodoForm and add the same time I'm adding a separate CSS file for TodoForm component. After creating all those files, your directory structure will look like below.

📦src
 ┣ 📂components
 ┃ ┗ 📂TodoForm
 ┃ ┃ ┣ 📜TodoForm.js
 ┃ ┃ ┗ 📜todoForm.module.css
 ┣ 📜App.js
 ┣ 📜app.module.css
 ┣ 📜index.css
 ┗ 📜index.js

Our TodoForm component will be a stateful component and we'll be managing state with React Hooks. with React hooks we will be able to bring the state into functional components. It's the latest features that new React version offers us. Let's use it inside our TodoForm and implement the Component ith the help of antd Input and form fields.

TodoForm.js
import React, { useState } from "react"
import { Form, Input, Button } from "antd"
//styles
import styles from "./todoForm.module.css"

const TodoForm = ({ submit }) => {
  const [state, setState] = useState({
    value: "",
    validated: false,
  })

  const handleSubmit = e => {
    e.preventDefault()
    if (state.value !== "") {
      submit(state.value)
      setState({ ...state, value: "", validated: false })
    }
  }

  const handleChange = e => {
    setState({
      ...state,
      value: e.target.value,
      validated: e.target.value.length > 5 ? true : false,
    })
  }

  return (
    <Form layout="inline" onSubmit={handleSubmit} className={styles.form}>
      <Form.Item>
        <Input
          onFocus={() => setState({ ...state, focused: true })}
          placeholder="Add Todos ..."
          value={state.value}
          onChange={handleChange}
          className={styles.input}
        />
      </Form.Item>

      <Button
        icon="plus-circle"
        type="success"
        htmlType="submit"
        disabled={!state.validated}
      >
        Add
      </Button>
    </Form>
  )
}

export default TodoForm

Understanding React Hooks

To use React Hooks, you must import it from react and call it inside a functional component. You may have seen that I have called the userState function right after the function declaration inside the function.

We have to pass 2 parameters as destructuring assignments when you call useState function and pass any initial state as an Object if you have any. In our case, I have passed value and validated attributes.

The state will always refer to the current state, and we will be using setState method to manipulate the state. You can give them any names you like, I gave state and setState because those are the names that we use inside class components.

For an exmaple you we can do :

const [values, setValues] = useState({
  value: "",
  validated: false,
})

It's totally up to you to decide what names that you are gonna use, but make sure you use something relative. Don't use random names as it will make other developers confuse 😳 in your compnay 🏢

Handling Events

As this is a small component we only got two events to handle,

  1. onChange when someone types a todo
  2. onSubmit when someone submits the todo form

Now let's see how we are going to handle those events with the help of our superhero React hooks 🔥

Lets deeply inspect our handleChange method

const handleChange = e => {
  setState({
    ...state,
    value: e.target.value,
    validated: e.target.value.length > 5 ? true : false,
  })
}

onChange Event

onChange method is mostly used with input fields. You can see that I have added an onChage method to the <Input> component. Whenever someone type on that field handleChange function will be run and It will receive the event object which is (e) in our case, that is passed from the Input field.

And I have implemented the setState() inside handleChange function so that it will change our state, every time the input value changes. The changed value is extracted from the e.target.value, e.terget will always refer to the Input element itself and value property will be equal to the values that we have typed.

Here I do some other thing, which is setting the validated property, I want the todo to be at least 5 characters long, that's why I check the length of the input value. Based on that value, I have added a disabled attribute to the submit button. Therefore, you won't be able to submit the todo unless you type a todo that is longer than 5 characters. Cool right? 😁

onSubmit Event

const handleSubmit = e => {
  e.preventDefault()
  if (state.value !== "") {
    submit(state.value)
    setState({ ...state, value: "", validated: false })
  }
}

This method is attached to our form element and will be fired when someone clicks the Add button inside the form. In here the submit method will be passed to the TodoForm component by App component and you can see that I have extracted it on the fly from props as a parameter with the help of destructuring.

Creating Todo Element

As we did last time when building TodoForm component, let's add a folder called Todo, Todo.js file and todo.module.css file.

📦src
 ┣ 📂components
 ┃ ┣ 📂Todo
 ┃ ┃ ┣ 📜Todo.js
 ┃ ┃ ┗ 📜todo.module.css
 ┃ ┗ 📂TodoForm
 ┃ ┃ ┣ 📜TodoForm.js
 ┃ ┃ ┗ 📜todoForm.module.css
 ┣ 📜App.js
 ┣ 📜app.module.css
 ┣ 📜index.css
 ┗ 📜index.js

Inside Todo component we will be needing 4 buttons for each Todo, which are to delete, mark as complete, revert a completed todo and to update. When a Todo is marked as completed, only undo and delete button will be shown and when a Todo is not completed, mark, delete and edit buttons will be shown.

Todo.js
import React, { useState } from "react"
import { Button, List, Input } from "antd"

//styles
import styles from "./todo.module.css"

const Todo = ({ todo, onComplete, onDelete, onUpdate, onUndo }) => {
  const { name, isComplete } = todo

  const [state, setState] = useState({
    currentlyEditing: false,
    inputValue: "",
  })

  const onEdit = () => {
    setState({ ...state, currentlyEditing: true, inputValue: name })
  }

  const onChange = e => {
    setState({
      ...state,
      inputValue: e.target.value,
    })
  }

  const onLocalUpdate = () => {
    setState({
      ...state,
      currentlyEditing: false,
    })
    onUpdate(state.inputValue)
  }

  return (
    <List.Item>
      <List.Item.Meta
        title={
          state.currentlyEditing ? (
            <Input
              className={styles.input}
              type="text"
              value={state.inputValue}
              onChange={onChange}
            />
          ) : (
            <p className={`${styles.text} ${isComplete ? styles.cut : ""}`}>
              {name}
            </p>
          )
        }
      />

      <Button.Group>
        {state.currentlyEditing ? (
          <Button
            className={styles.editConfirm}
            icon="check"
            onClick={state.currentlyEditing ? onLocalUpdate : onComplete}
            disabled={state.inputValue.length <= 5}
          />
        ) : !isComplete ? (
          <>
            <Button onClick={onComplete} icon="check" />
            <Button onClick={onEdit} icon="edit" />
            <Button onClick={onDelete} icon="delete" />
          </>
        ) : (
          <>
            <Button onClick={onUndo} icon="undo" />
            <Button onClick={onDelete} icon="delete" />
          </>
        )}
      </Button.Group>
    </List.Item>
  )
}

export default Todo

This component is also implemented as a functional component with the help of React Hooks and different buttons are displayed according to the isComplete property that each todo has got. When the edit button is clicked currentlyEditing value is set to true inside onEdit and the static text will become an Input element and will give you the ability to edit the text. Changes are recorded with the help of React Hooks implemented inside handleChange method. onComplete, onDelete, undo methods will be implemented inside the App component.

Finalizing App Component's Methods

App component is a class-based component and it's the largest in this project, it's because it holds the main state of the application plus has so many methods inside to handle various changes happening.

App.js
import React, { Component } from "react"
import randomId from "random-id"
import produce from "immer"
import { Helmet } from "react-helmet"
import { List, Card } from "antd"

//custom components
import Todo from "./components/Todo/Todo"
import TodoForm from "./components/TodoForm/TodoForm"

import { Button } from "antd"

//styles
import styles from "./app.module.css"

class App extends Component {
  constructor(props) {
    super(props)

    this.onComplete = this.onComplete.bind(this)
    this.onDelete = this.onDelete.bind(this)
    this.addTodo = this.addTodo.bind(this)
    this.onUpdate = this.onUpdate.bind(this)
    this.clearAll = this.clearAll.bind(this)
    this.onUndo = this.onUndo.bind(this)
    this.storeTodos = this.storeTodos.bind(this)
  }

  state = {
    todos: [],
  }

  componentDidMount() {
    let existingTodos = JSON.parse(localStorage.getItem("todos"))
    let fetchededTodos = existingTodos !== null ? existingTodos : []
    this.setState({ todos: fetchededTodos })
  }

  addTodo(todo) {
    const newTodos = [
      { name: todo, isComplete: false, id: randomId() },
      ...this.state.todos,
    ]

    this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
  }

  onDelete(index) {
    if (window.confirm(`Are you sure that you want to delete it?`)) {
      let newTodos = [...this.state.todos]
      newTodos.splice(index, 1)
      this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
    }
  }

  onComplete(index) {
    let newTodos = [...this.state.todos]
    newTodos[index].isComplete = true
    this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
  }

  onUpdate(value, index) {
    const newTodos = [...this.state.todos]
    newTodos[index].name = value

    this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
  }

  onUndo(index) {
    let newTodos = [...this.state.todos]
    newTodos[index].isComplete = false
    this.setState(newTodos, () => this.storeTodos(newTodos))
  }

  storeTodos(newTodos) {
    localStorage.setItem("todos", JSON.stringify(newTodos))
  }

  clearAll() {
    if (
      window.confirm(
        `Are you sure that you want to clear all todos? This can't be undo later!`
      )
    ) {
      this.setState({ todos: [] }, () => this.storeTodos([]))
    }
  }

  render() {
    return (
      <>
        <Helmet>
          <meta charSet="utf-8" />
          <title>My Todos</title>
        </Helmet>
        <Card
          className={styles.app}
          title="My Todos"
          extra={
            <Button
              onClick={this.clearAll}
              icon="delete"
              type="danger"
              disabled={this.state.todos.length === 0 ? true : false}
            >
              Clear All
            </Button>
          }
        >
          <TodoForm submit={todo => this.addTodo(todo)} />
          <List
            dataSource={this.state.todos}
            renderItem={(item, index) => (
              <Todo
                key={item.id}
                todo={item}
                onComplete={() => this.onComplete(index)}
                onDelete={() => this.onDelete(index)}
                onUpdate={value => this.onUpdate(value, index)}
                onUndo={() => this.onUndo(index)}
              />
            )}
          />
        </Card>
      </>
    )
  }
}

export default App

Lets go method by method,

addTodo Method

 addTodo(todo) {
    const newTodos = [
    { name: todo, isComplete: false, id: randomId() },
    ...this.state.todos,
    ]

    this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
 }

addTodo method is connected with TodoForm element fired when someone submits the form with a todo. The todo is passed as an argument to this function and it will be prepended to the current list of todos stored as an array.

onComplete Method

onComplete(index) {
    let newTodos = [...this.state.todos]
    newTodos[index].isComplete = true
    this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
 }

This method will be fired when someone clicks on the check button that we implemented inside Todo component, and all these methods are passed to Todo component with props. When this method is fired, the isComplete property of the selected todo will be set to true and the selected todo is identified with the help of index of the todos array and another thing is that here I have used spread operator in order to clone the current todo, because we should not directly manipulate the state.

onDelete Method

onDelete(index) {
    if (window.confirm(`Are you sure that you want to delete it?`)) {
        let newTodos = [...this.state.todos]
        newTodos.splice(index, 1)
        this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
    }
 }

onDelete method is fired when someone clicks on the delete button and It will remove the selected todo from the app. Array splice method has been used to delete one item from the todos array.

onUpdate Method

onUpdate(value, index) {
    const newTodos = [...this.state.todos]
    newTodos[index].name = value

    this.setState({ todos: newTodos }, () => this.storeTodos(newTodos))
 }

onUndo Method

onUndo(index) {
    let newTodos = [...this.state.todos]
    newTodos[index].isComplete = false
    this.setState(newTodos, () => this.storeTodos(newTodos))
 }

When this method is executed, what is does it that it sets the isComplete property of the selected todo to true and regular setState method of React is used to set updated todos as current todos.

clearAll Method

clearAll() {
    if (window.confirm(`Are you sure that you want to clear all todos? This can't be undo later!`)) {
        this.setState({ todos: [] }, () => this.storeTodos([]))
    }
}

Well, This method we give the user the ability to clear all todos at once. When someone clicks the Clear All button they will be prompted with a confirmation popup and if they confirm it an empty array will be set as the sate which removes all recorded todos.

Permanently Storing Todos

We have reached to the end of the application, now all we have to do is to store our todos someone permanent, because now if someone reloads the page after storing some todos, all of them will be gone. The component state is not persistent. That is why we have to use databases to store our data.

As this is a small project and we don't have a huge amount of data, we can use the localstorage which comes inbuilt to all browsers. We can identify it as a simple little database that is available inside browsers.

So, what we do here is getting the previously stored items when App component is rendred and updating it each time when our sate changes to match it to the current state.

After passing the state to the setState method, we can pass a callback function to runs when it completes setting the state. That's a good place to store our state in localstorage.

storeTodos(newTodos) {
    localStorage.setItem("todos", JSON.stringify(newTodos))
}

In our application, you can see that I have implemented a function named storeTodos , and it's passed as a callback to all setState methods with the new state. setItem method can be used to store data into localStorage and getItem can be used to fetch stored Items. We can only store key-value pairs inside local storage and that's the reason that I have stringify our state array before storing.

All stored todos are fetched inside componentDidMount method.

componentDidMount() {
    let existingTodos = JSON.parse(localStorage.getItem("todos"));
    let fetchededTodos = existingTodos !== null ? existingTodos : [];
    this.setState({ todos: fetchededTodos });
}

As we get stringify version of our todos we must parse it to be a real Array before setting as the state.

Conclusion

I guess that you learned how to build a simple yet powerful todo app with the help of React, React Hooks and Ant Design UI Library. Fell free to comment if you have any questions. Good luck and happy coding until I meet you guys with the next article. 👋 👋 👋