Deleting task(s) in our to-do list app [part-2 ]

Deleting task(s) in our to-do list app [part-2 ]

Β·

13 min read

This article is a continuation of our to-do list app.

Check out part-1 and part-3.

Within the li element of each task, we want to display a trash icon. And use the icon to delete that very li element.

First, we need to download Heroicons which is a package or library for SVG icons.

In the terminal, run the command npm install @heroicons/react. (You can stop the server run the command then restart the server with the script npm run dev . Or you can open a new instance using the + icon in the Terminal as we did before)

npm install @heroicons/react

We then need to import an icon, TrashIconthat we will use in our project.

For your next project, you can find heroicons names at unpkg.

Now let us import the icon in our App.js

import { useState } from 'react'
import { v4 as uniqueId } from 'uuid'
//import the icon
import { TrashIcon } from '@heroicons/react/24/outline'

Now let us add the icon in our li element.

          <ul className='ul-container'>
            {tasks.length > 0 ? (
              tasks.map((task) => {
                const { id, title } = task
                return (
                  <li key={String(id)} className='task-container'>
                    {title}
                    <TrashIcon />
                  </li>
                )
              })
            ) : (
              <p className='empty-tasks'>No tasks added yet</p>
            )}
          </ul>

After adding a task on the browser, you should see the icon.

Let us add a utility class trash-icon to our icon.

<ul className='ul-container'>
     {tasks.length > 0 ? (
        tasks.map((task) => {
          const { id, title } = task
          return (
                <li key={String(id)} className='task-container'>
                    {title}
                    <TrashIcon className='trash-icon' />
                </li>
                )
           })
          ) : (
               <p className='empty-tasks'>No tasks added yet</p>
            )}
</ul>

In index.css, let us apply styles to the icon using the utility class trash-icon

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer utilities {
  /* colors */
  .pri-color {
    color: #ffffff;
  }
  .bg-pri {
    background-color: rgb(12, 74, 110);
  }
  /* css for elements */
  .parent-container {
    @apply flex justify-center items-center min-h-screen;
  }
  .content {
    @apply bg-black p-7 rounded-2xl  flex justify-around items-center flex-col;
  }
  .input-label {
    @apply text-2xl pri-color px-3 font-semibold;
  }
  .add-btn {
    @apply text-lg bg-pri rounded-lg px-3 py-1 ml-3 pri-color font-medium;
  }
  .ul-container {
    @apply py-5 list-none;
  }
  .task-container {
    @apply flex justify-between pri-color bg-pri py-1 px-3 rounded-md text-lg my-2;
  }
  .empty-tasks {
    @apply pri-color p-1 text-lg;
  }
  /* add out class */
  .trash-icon {
    @apply w-5 h-5 hover:scale-125 transition;
  }
}

In the browser, if you add a task, you should see the icon displayed in a reasonable size.

Now that we have the icon, we need to use it in deleting the list element attached to the icon.

Deleting icon functionality.

Let us create a clearSingleTask function within our App component in App.js

It doesn't matter where place the function as long as it is within the App component but not below the return.

In JavaScript, functions are hoisted to the top of their scope, so you can define a function after it is used in your code.

Let's place the clearSingleTask function below handleSubmit. This will help us read and maintain our code easily.

  const handleSubmit = (e) => {
    e.preventDefault()
    const inputValue = e.target.taskInput.value.trim()
    if (inputValue) {
      const taskObject = {
        id: uniqueId(),
        title: inputValue,
      }
      setTasks([...tasks, taskObject])
      e.target.taskInput.value = ''
    } else {
      alert('Can not add an empty task')
    }
  }

// our function
 const clearSingleTask = () => {
     console.log('Clear btn clicked!')
 }

Make sure to comment out or delete our previous console.log(tasks). πŸ‘‡

  const [tasks, setTasks] = useState([])
  // log tasks
  // console.log(tasks)

The takeaway from the tutorial!

Take it slow... If you understand this part then you're done. Just apply for a React frontend jobπŸ˜‚πŸ˜‚

Let us attach an onClick event to our TrashIcon and reference the handler function clearSingleTask that we created.

          <ul className='ul-container'>
            {tasks.length > 0 ? (
              tasks.map((task) => {
                const { id, title } = task
                return (
                  <li key={String(id)} className='task-container'>
                    {title}
                    <TrashIcon
                      className='trash-icon'
                      onClick={clearSingleTask}
                    />
                  </li>
                )
              })
            ) : (
              <p className='empty-tasks'>No tasks added yet</p>
            )}
          </ul>

In the browser, reload your app. The console should be empty.

Add a task.

The console should still be empty.

Now click on the TrashIcon in your task.

You should see a message 'clear btn clicked' on the console.

Let us think about the icon functionality.

  1. The user clicks the TrashIcon component of a particular task on the app.

  2. The onClick event invokes the handler function clearSingleTask

  3. When clearSingleTrash is triggered, the console log inside the function console.log('Clear btn clicked!') runs.

  4. We want to get the task bound or linked to the TrashIcon. That's why we need to create a parameter for the handler functionclearSingleTrash then in our TrashIcon we pass in an argument when we reference the handler function in the onClick event.

  5. Let's create a parameter that will receive the argument from TrashIcon and pass it to clearSingleTask. I called it just id.

    The name of the parameter in clearSingleTask can be anything. The most important thing is the argument we receive when the clearSingleTask is 'referenced'. I wrote 'referenced' in quotes. πŸ“ŒPut a pin on that. We will revisit that later.

    Let's also log the parameter idπŸ‘‡

       const clearSingleTask = (id) => {
         console.log(id)
       }
    
  6. Lastly, in our TrashIcon, we want to get the id we destructured from the task and pass it as an argument to the handler function clearSingleTask that is passed to the onClick event.

    This is the most confusing part at least for most new developers. The TrashIcon doesn't know whether it is deleting the task. It only gets access to the destructured id and passes it to the function clearSingleTask which then uses the id to update the state as we'll see later. It is through updating the state that a task is deleted! Remember the TrashIcon is sitting in the li and has access to the id destructed from the task. Then the TrashIcon passes the id to the onClick event which passes the id to the handler function clearSingleTaskduring invocation (id) as an argument. Then we can now use the id in the handler function clearSingleTask to update the state.

    Now pass the id as an argument to the handler function like thisπŸ‘‡

<TrashIcon className='trash-icon' onClick={clearSingleTask(id)}
 />

We are done with the setup. Now we should go to the handler function clearSingleTask and set the functionality of what we want the TrashIcon to do when clicked.

Before we implement our clearSingleTask functionality, let us add a task and see what happens.

The id gets logged to the Browser console even without us clicking on the TrashIcon. And if you try to click the button thereafter, nothing happens.

Didn't we pass the handler function to the onClick event in the TrashIcon? Of course, we did. Why this behavior?

This is our TrashIcon code.

<TrashIcon
className='trash-icon'
onClick={clearSingleTask(id)}
/>

Previously when we hadn't set the parameter, the onClick event was working fine. We were seeing our console log after clicking the TrashIcon.

(when our code looked like thisπŸ‘‡)

<TrashIcon
className='trash-icon'
onClick={clearSingleTask}
/>

It worked because we were only referencing the handler function clearSingleTask without invoking () it.

πŸ“ŒI told you to put a pin on 'referencing'

If you do onClick={handle}, we say that you are referencing the function since there is no parenthesis () at the end of the handler function handle. The element with the onClick event needs to be clicked to trigger the function.

But if you add the parenthesis, onClick={handle()}, you are calling the function handle immediately when the component is rendered, instead of passing it as a reference to be called when the onClick event occurs.

...end of pinπŸ”š

In a nontech explanation, since the onClick event doesn't see () at the end of clearSingleTask, it just tells the reference 'you have been called. This is because we haven't told the onClick event to invoke it. It will only do so if it sees () πŸ˜‚.

In our current scenario with onClick={clearSingleTask(id)}, we are invoking the function right away. So once the TrashIcon mounts, the clearSingleTask function is invoked. This is because of the parenthesis (id).

From JavaScript, we know that functions are invoked by putting a parenthesis at the end of the function when calling it.

How do we prevent the event from invoking the handler function once the TrashIcon is rendered/mounted?

We want the event to be triggered only once the TrashIcon is clicked and not when the TrashIcon is rendered/mounted/displayed.

By using a callback function.

We can just use a shorter es6 arrow function syntax. Like this πŸ‘‡.

<TrashIcon
className='trash-icon'
onClick={() => clearSingleTask(id)}
/>

The () is an anonymous function. That's the function.

We use the anonymous function because we want to pass in our argument to the next function clearSingleTask without invoking it once we pass an id argument.

Basically, we have two functions in the onClick event.πŸ‘‡

In a nontech explanation, it is like we are saying to the onClick event, "since you like invoking any function you see that has (), we will give you a function to invoke once our component mounts then you'll now wait for our instructions on what to do for the second function." Then we give it an anonymous ()function (a function without a name)πŸ˜‚. Our anonymous function has no logic in it, so nothing happens after it is invoked. The onClick event stops after invoking the first function () since it has no direct access to clearSingleTask function. It will only invoke it if we trigger a click event with the TrashIcon.

So now when the TrashIcon is rendered, the anonymous function is invoked immediately then the second function remains pending and will only be executed on clicking the TrashIcon.

Let's go back to our app in the browser and add a task.

Nothing is logged in the browser console.

Now click on the TrashIcon in our task li item.

In the browser, you should see the task id logged to the console.

Our TrashIcon works the way it is supposed to. We are done with setting up the TrashIcon.

It is time to add functionality to the handler function that we passed to TrashIcon's onClick event. By doing so, we will dictate what the TrashIcon should do or rather what should happen when one clicks the TrashIcon.

Let's head over to our handler function clearSingleTask. At the moment, we are just logging the id to the browser console.πŸ‘‡

  const clearSingleTask = (id) => {
    console.log(id)
  }

We want to take the id parameter, which is of course passed in by the TrashIcon as an argument and use it to delete the task.

Important! Let me repeat for the second time, the handler function clearSingleTask in the onClick event in our TrashIcon gets access to the id when we destructure the task in our li item because the TrashIcon sits within the li item.

Once we get the id from the TrashIcon we want to use that id to filter through our tasks and do something with the filtered result. in our case, delete the task.

First, let's do a filter test and see what we get.

const clearSingleTask = (id) => {
const filteredTask = tasks.filter((eachTask) => id === eachTask.id)
console.log(filteredTask)
}

What is happening in the code above?

We get the id from our TrashIcon then we filter through all the tasks.

Remember each task is stored in our tasks state that we set in part-1 of this article. In our filter, we represent each task with eachTask then for every task we get the id of the task with eachTask.id and compare that id with the id we get from the TrashIcon. We set the filtered result to a variable filteredTask so that we can easily use the result we get. In our case, we want to log the result to the browser console.

We used strict equals === because we want to compare both the value and the type of the IDs.

In our browser app, add a few tasks like two or three then click the TrashIcon on each.

You should see the task logged to the console as an object. Because we set all tasks as objects with id and title properties.

Now we know that our TrashIcon gets us the correct task.

We want to delete the task that has it TrashIcon clicked.

This seems a little bit tricky let me show you the code first then explain .πŸ‘‡

const clearSingleTask = (id) => {
const filteredTask = tasks.filter((eachTask) => id !== eachTask.id)
setTasks(filteredTask)
}

The filter() is an array method that always returns a new array that contains all elements of the original array that pass a given test.

In our context, filteredTask is the new array returned by the filter() method, which contains all tasks in the tasks array whose id is not equal to the id provided by the parameter in clearSingleTask handler function. From our previous console log, you see the TrashIcon has access to the id of the task we click.

In simple terms, it returns a new array containing only those tasks whose id is not equal to the id from TrashIcon and assign it to a variable filteredTask.

Note: We used the strict inequality operator !== because we want to check whether two values are not equal in both value and type.

We then set our tasks state with our filtered tasks using setTasks(filteredTask).

Now when you click on the TrashIcon you update the state of our tasks to contain only those tasks without the id passed down from the TrashIcon. In that case, you deleted the task!

Open our browser app, add a few tasks and click on the TrashIcon.

You should be able to successfully delete each task.πŸ™Œ

What if you had 10 tasks want to delete all of them at once?

Let's fix that.

In our App.jsx code, go below the form but still within the div and add a button with a className clear-btn and some text content like Clear All Tasks .

<button className='clear-btn'>Clear Tasks</button>

Now go to index.css and use the utility class clear-btn to add styling.

Like thisπŸ‘‡

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer utilities {
  /* colors */
  .pri-color {
    color: #ffffff;
  }
  .bg-pri {
    background-color: rgb(12, 74, 110);
  }
  /* css for elements */
  .parent-container {
    @apply flex justify-center items-center min-h-screen;
  }
  .content {
    @apply bg-black p-7 rounded-2xl  flex justify-around items-center flex-col;
  }
  .input-label {
    @apply text-2xl pri-color px-3 font-semibold;
  }
  .add-btn {
    @apply text-lg bg-pri rounded-lg px-3 py-1 ml-3 pri-color font-medium;
  }
  .ul-container {
    @apply py-5 list-none;
  }
  .task-container {
    @apply flex justify-between pri-color bg-pri py-1 px-3 rounded-md text-lg my-2;
  }
  .empty-tasks {
    @apply pri-color p-1 text-lg;
  }
  .trash-icon {
    @apply w-5 h-5 hover:scale-125 transition;
  }
  /* our new style for the clear all btn πŸ‘‡ */
  .clear-btn {
    @apply text-xl block w-full bg-red-900 hover:opacity-80 transition rounded-lg px-3 py-1 pri-color font-medium;
  }
}

In the browser, you should see the button displayed below your tasksπŸ‘‡

Now we need to add some functionality to our Clear All Tasks button.

In our button, let's add an onClick event and pass in a handler function. I called mine clearAllTasks.

<button className='clear-btn' onClick={() => clearAllTasks()}>

In our, App.jsx above the return, let create the function clearAllTasks. You can place it anywhere within the App component but not after the return.

side note βœπŸ“

You must invoke the handler function clearAllTasks after passing it to the onClick event even if we are not going to have parameters and arguments to pass in!

Using onClick={() => clearAllTasks} will only return the function reference without executing it, so it will not clear the tasks.

Therefore, you should use, onClick={() => clearAllTasks()}, to call the function and execute it properly.

...end of side noteπŸ”š

I placed mine below our clearSingleTask function. Like thisπŸ‘‡

const clearSingleTask = (id) => {
const filteredTask = tasks.filter((eachTask) => id !== eachTask.id)
setTasks(filteredTask)
}
// our function πŸ‘‡πŸ‘‡πŸ‘‡πŸ‘‡
const clearAllTasks = () => {

}

Now let's add some functionality to the clearAllTasks function.

What should happen to the tasks when we click on the button?

We should clear all the tasks in our web app. Just like clearing a single task, clearing all tasks means updating the state of our tasks.

We will set the state of our tasks to an empty array []. It is that simple!

const clearAllTasks = () => {
   setTasks([])
}

Now go to the browser, add some tasks and click on the Clear All Tasks button.

Were you able to clear all the tasks?

Mine workedπŸ‘‡

Here is what happened:

You clicked on the Clear All Tasks button. The button updated our tasks state to an empty array [].

In that regard, we had to tasks to iterate over and display. Hence you deleted all tasksπŸ˜‚

We are done with the task deletion part.

Check out part-3 where we modify our app to accept many tasks at once.

Β