Creating a To Do App

Creating a To do app.

About the To Do app

At the centre of your To Do app is an array of objects named todoList. Each object represents a task with three properties:

  • id:
  • taskName:
  • completed:

The third is a Boolean value.

You will add functionality enabling users to:

  • View all current tasks
  • Add new tasks
  • Delete tasks
  • Mark tasks as 'completed'.

Starting with create-react-app

Follow these steps to create your basic app structure with the create-react-app script.

  1. Open a new Command prompt window or, in VS Code, a new Terminal.
  2. Navigate to where you want ReactJS to create a folder for your app. For example:
    C:\> users\JohnSmith
  3. Type the following command that includes the name you want to call your new app. For example, todos.
    C:\> npx create-react-app todos

After you have created your app, you launch it by running the following command from inside your new app folder.

npm start

A new browser window should display with your new app running on the ReactJS local development server. If not, open a new browser tab and enter http://localhost:3000.

You will see a screen similar to the following.

ReactJS sample screen

Customising your CRA-created content

Now you will customise the 'boilerplate' content provided by the create-react-app (CRA) script.

The /public folder

  1. In your app’s /public folder, open the index.html file and replace all its content with the following.
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <title>Single Page Application Blog App</title>
      <meta name="description" content="An SPA blog app created with ReactJS" />   
      <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    </head>
     
    <body>
     
      <div id="root"></div>
           
    </body>
    </html>

The /src folder

  1. In the /src folder, open the index.js file and replace all its content with the following.
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App';
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    
    root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
    );
  2. And again in the /src folder, open the App.js file and replace its content with the following.
    function App() {
    return (
      <>
      <h1>Hello, World!</h1>
      <p>Paragraph of text.</p>
      </>
    );
    }	
    export default App;

Your web page should now look as shown below.

Sample React screen

Download the CSS file.

Download the file below to your /src folder, where it will overwrite the file of the same name created by the create-react-app script.

App.css

Creating the two UI elements

The UI of the app will have two areas:

  • An upper part to hold an user input field and a button.
  • A lower part to list tasks already entered by the user.

Update the App.js function as follows.

function App() {
   return (
    { /* area for user input of tasks */ }
    <div className="addTask">
       <input />
       <button>Add Task</button> 
    </div>
    
    { /* area for listing existing tasks */ }
    <div className="list"></div>
   );
}	
export default App;

Add state to the app

Next, create an array named todoList to hold the list of user-entered tasks. The list will update as new tasks are added and existing ones deleted or marked as 'completed'.

On the first line of the App() function, add the following line:

const [todoList, setTodoList] = useState([]);

And at the top of the file, add this import statement:

import { useState } from "react"
  • The todoList variable will hold the current tasks in the array.
  • The setTodoList function will update the array with a new task.
  • The initial value of the useState() hook is [], an empty array.

Managing user input

Create a second state to handle a new task entered by the user.

const [newTask, setNewTask] = useState("");
  • The newTask variable represents the value in the input field.
  • The setNewTask function will add this user-entered task to the array.
  • The initial value of the useState() hook is "", an empty string.

Handling the Add Task button

Update the input field as follows:

<input onChange={handleChange} />

And add the following function within the App() function to handle the change event.

const handleChange = (event) = >: {
   setNewTask(event.target.value);
}

Adding a new task to the array

Capture a user's click on the Add Task button by adding the following event handler to the button.

<button onClick={addTask}>Add Task</button>

And add the following function within the App() function to handle the button click event.

const addTask = () => {
    setTodoList([...todoList, newTask]);
}

This function uses the spread operator to the new task to the tasks already in the array.

Displaying the list of tasks

Now you are ready to display the list of tasks in the array. Update the second div element as follows:

{ /* area for listing existing tasks */ }
    <div className="list">
    {todoList.map(task => {
        return <div><h1>{task}</h1></div>
        })
    }
    </div>

Add the option to delete tasks

Update the lower part of the UI as follows:

{ /* area for listing and deleting existing tasks */ }
<div className="list">
   {todoList.map(task => {
      return (
         <div>
           <h1>{task}</h1>
           <button onClick={() => deleteTask(task)}> X </button>
         </div>
        )
      })
   }
</div> 

To the App function add this new event handler function:

const deleteTask = (taskName) => {
    setTodoList(todoList.filter(task => task !== taskName));
 }

You can now delete tasks from the displayed list.

Unfortunately, if multiple tasks share the same name - they will all be deleted.

Improving the task delete option

The solution is to add a unique identifier or id to each task added to the list.

To do this, update the array that stores the tasks, named todoList, from an array of strings to an array of objects.

For each object in the array, you can now store both a name (which may not be unique) and an id (which will be unique). See below.

{
    id: 1,
    taskName: "Buy milk"
} 

Currently, the addtask() function only adds a string and not an object.

const addTask = () => {
    setTodoList([...todoList, newTask]);
}

Update this function as follows:

const addTask = () => {
    const task = {
        id: todoList.length === 0 ? 1 : todoList[todoList.length - 1].id + 1,
        taskName: newTask
    };
    setTodoList([...todoList, task]);
};

Update the UI as follows:

<h1>{task.taskName}</h1>
<button onClick={ () => deleteTask(task.id) } > X </button>

Finally, update the delete function as follows:

const deleteTask = (id) => {
   setTodoList(todoList.filter(task => task.id !== id));
}

Adding components and props

Next we will reorganise the app by extracting the highlighted UI block below into a separate component.

First, create a new file named Task.js in the /src folder and add the following content to it.

export const Task = (props) => {
    return (
        <div><h1>{props.taskName}</h1>
        <button onClick={() => deleteTask(props.id)}> X </button>
        </div>
    );
}

In the App.js file, update the lower UI as follows:

<div className="list">
   {todoList.map(task => {
        return (
            <Task
                id={task.id}
                taskName={task.taskName}
            />
        )
    })
    }

As you can see, you are passing the id and taskName props from the App.js parent component to the child Task.js component.

Add this import statement to App.js.

import { Task } from "./Task";

Next, we need to make the deleteTask() function in the App.js available to the Task.js component.

We can do this by passing the function as a prop to Task.js.

Update Task.js as follows:

export const Task = (props) => {
        return (
            <div><h1>{props.taskName}</h1>
            <button onClick={() => props.deleteTask(props.id)}> X </button>
            </div>
        );
    }

In the App.js file, update the lower UI as follows:

<div className="list">
   {todoList.map(task => {
        return (
            <Task
                id={task.id}
                taskName={task.taskName}
                deleteTask = {deleteTask}
            />
        )
    })
    }

Adding a task status option

Your final exercise is to add the ability to mark a displayed task as 'complete' and change its colour to green.

In the App.js file, add a third field named completed to the task object as follows:

const addTask = () => {
    const task = {
        id: todoList.length === 0 ? 1 : todoList[todoList.length - 1].id + 1,
        taskName: newTask,
        completed: false
    };
    setTodoList([...todoList, task]);
};

Add this new function to App.js.

const completeTask = (id) => {
    setTodoList(todoList.map(task => {
        if (task.id === id) {
            return { ...task, completed: true };
        }
        else {
          return task;
        }
    }));
}

Pass this function and the new property as props to the Task.js component.

<div className="list">
   {todoList.map(task => {
        return (
            <Task
                id={task.id}
                taskName={task.taskName}
                completed = {task.completed}
                deleteTask = {deleteTask}
                completeTask = {completeTask}
            />
        )
    })
    }

Update the Task.js components as follows:

export const Task = (props) => {
return (
    <div 
            className="Task"
            style={ {backgroundColor: props.completed ? "transparent" : "pink" } }>
            <h1>{props.taskName}</h1>
    
            <button onClick={() => props.completeTask(props.id)}> Complete </button>
    
            <button onClick={() => props.deleteTask(props.id)}> X </button>
    </div>
        );
}

App.js

Task.js