This article is a continuation of our to-do list app.
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, TrashIcon
that 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.
The user clicks the TrashIcon component of a particular task on the app.
The onClick event invokes the handler function
clearSingleTask
When
clearSingleTrash
is triggered, the console log inside the functionconsole.log('Clear btn clicked!')
runs.We want to get the task bound or linked to the TrashIcon. That's why we need to create a parameter for the handler function
clearSingleTrash
then in ourTrashIcon
we pass in an argument when we reference the handler function in the onClick event.Let's create a parameter that will receive the argument from
TrashIcon
and pass it toclearSingleTask
. I called it justid
.The name of the parameter in
clearSingleTask
can be anything. The most important thing is the argument we receive when theclearSingleTask
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) }
Lastly, in our
TrashIcon
, we want to get theid
we destructured from thetask
and pass it as an argument to the handler functionclearSingleTask
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 thetask
. It only gets access to the destructuredid
and passes it to the functionclearSingleTask
which then uses theid
to update the state as we'll see later. It is through updating the state that a task is deleted! Remember theTrashIcon
is sitting in theli
and has access to theid
destructed from the task. Then theTrashIcon
passes theid
to the onClick event which passes theid
to the handler functionclearSingleTask
during invocation(id)
as an argument. Then we can now use theid
in the handler functionclearSingleTask
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.