A ToDo App is a very beginner level app for any frontend developer. A basic ToDo app has functionality of adding, deleting and updating the todos from the list. Being a developer, we easily tend to forget the tasks for the day or a span of time. It's always advisable to have such an app where we can add or delete or modify todos.
In this tutorial, let's design a ToDo App from scratch with basic crud (Create, Read, Update, Delete) functionality and added features like filter search, hide todos, and update status.
Getting Started
Creating a React App from cra-template using create-react-app
, we will require no external libraries for the project, except react-icons
that we will need for icons used in the application.
ToDoApp.jsx
import React from 'react';
export default function ToDoApp() {
return (
<section className="ToDoApp">
<h1>ToDo App</h1>
</section>
);
}
We will implement two components namely ToDoCard and ToDoForm for the app.
Implementation
Adding Basic Styles
ToDoApp.css
.ToDoApp {
width: 800px;
max-width: 100%;
margin: auto;
padding: 0.5rem;
color: var(--black);
}
.grey_text {
color: var(--grey);
}
.red_text {
color: var(--red);
}
.blue_text {
color: var(--blue);
}
.green_text {
color: var(--green);
}
.ToDoApp input,
.ToDoApp textarea,
.ToDoApp select {
width: 100%;
padding: 0.5rem 0.75rem;
}
.ToDoApp textarea {
height: 10rem;
}
.ToDoApp button {
padding: 0.5rem 1.5rem;
background: var(--white);
border: 1px solid var(--black);
}
.ToDoApp__Search {
margin-top: 0.5rem;
display: flex;
gap: 1.5rem;
}
.ToDoApp__Search input {
border: 1px solid var(--black);
}
/* @ToDoList Layout */
.ToDoList {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 0.5rem;
}
.ToDoList__action {
width: 100%;
}
Card Component
Before we get started, let's install react-icons
by executing
npm i react-icons
Defining the json schema for each todo
{
"title": "string",
"description": "string",
"status": "integer(0,1,2)",
"hide": "boolean",
"id": "integer"
}
ToDoCard.jsx
import React from 'react';
// Icons for Todo Card
import {
FaCheckCircle,
FaClock,
FaExclamationCircle,
FaEye,
FaEyeSlash,
FaPencilAlt,
FaTimesCircle,
} from 'react-icons/fa';
export default function ToDoCard({
id,
title,
description,
status,
hide,
...otherProps
}){
// Checking if the card is to be hidden
if (hide) return null;
return (
<div className="ToDoCard" {...otherProps}>
<div className="ToDoCard__left">
<span>
{status === 0 && <FaExclamationCircle title="Pending" className="ToDoCard__icon grey_text" />}
{status === 1 && <FaClock title="Working" className="ToDoCard__icon blue_text" />}
{status === 2 && <FaCheckCircle title="Done" className="ToDoCard__icon green_text" />}
</span>
</div>
<div className="ToDoCard__center">
<h2>{title}</h2>
<p>{description}</p>
</div>
<div className="ToDoCard__right">
<FaTimesCircle
className="ToDoCard__icon red_text"
/>
<span>
<FaEye title="Show Description" className="ToDoCard__icon" />
</span>
<FaPencilAlt
className="ToDoCard__icon"
/>
</div>
</div>
);
}
The ToDoCard component takes all properties of ToDo schema, where hide is used to return null on true and status shows three different symbols on three different integer values.
Furthermore we can toggle description using a state variable,
ToDoCard.jsx
...
export default function ToDoCard({
...
}){
const [showDescription, setShowDescription] = React.useState(false);
...
return (
<div className="ToDoCard" {...otherProps}>
...
<div className="ToDoCard__center">
<h2>{title}</h2>
{showDescription && <p>{description}</p>}
</div>
<div className="ToDoCard__right">
...
<span
onClick={() => {
setShowDescription(!showDescription);
}}
>
{showDescription && <FaEye title="Show Description" className="ToDoCard__icon" />}
{!showDescription && <FaEyeSlash title="Hide Description" className="ToDoCard__icon" />}
</span>
...
</div>
</div>
);
}
Using React.useState(), we solve the problem of visibility of description and its toggling.
Styling the card is less of a trouble,
ToDoApp.css
...
/* @ToDo Card Layout */
.ToDoCard {
border: 1px solid var(--black);
width: 900px;
max-width: 100%;
padding: 0.5rem;
font-size: 1rem;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.ToDoCard div {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.ToDoCard .ToDoCard__left {
flex: 0 2.5rem;
}
.ToDoCard .ToDoCard__center {
flex: 3;
display: inline-block;
}
.ToDoCard .ToDoCard__right {
flex: 0 2.5rem;
gap: 0.5rem;
}
.ToDoCard h2 {
font-size: larger;
}
.ToDoCard__icon {
cursor: pointer;
}
@media screen and (max-width: 900px) {
.ToDoCard {
width: 100%;
flex-direction: column;
}
.ToDoCard div {
flex-direction: row;
justify-content: flex-start;
}
}
Show/Hide Cards with Limit
In this section, we use a state variable todos to store the value of todos and a variable maxDisplayTodos for defining max no of visible todo cards.
ToDoApp.jsx
import React from 'react';
import ToDoCard from './ToDoCard';
import './ToDoApp.css';
import { FaPlusCircle } from 'react-icons/fa';
export default function ToDoApp() {
const [todos, setTodos] = React.useState([]);
const [hideTodos, setHideTodos] = React.useState(true);
const maxDisplayTodos = 5;
React.useEffect(() => {
setTodos([
{
title: 'Learn React',
description: 'Learn React and its ecosystem',
status: 0,
hide: false,
id: 1,
},
{
title: 'Create a React Component',
description:
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Veritatis esse aut similique reprehenderit fuga cupiditate porro. Nostrum, ipsam perferendis! Fuga nisi nostrum odit nulla quia, sint harum eligendi recusandae dolore!',
status: 0,
hide: false,
id: 2,
},
{
title: 'Learn Vue',
description:
'Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary.',
status: 0,
hide: false,
id: 3,
},
{
title: 'Learn Angular',
description:
'A wonderful serenity has taken possession of my entire soul, like these sweet mornings of spring which I enjoy with my whole heart. I am alone, and feel the charm of existence in this spot, which was created for the bliss of souls like mine. I am so happy, my',
status: 0,
hide: false,
id: 4,
},
{
title: 'Vue Typewriter',
description:
'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta.',
status: 0,
hide: false,
id: 5,
},
{
title: 'Learn jQuery',
description:
'Li Europan lingues es membres del sam familie. Lor separat existentie es un myth. Por scientie, musica, sport etc, litot Europa usa li sam vocabular. Li lingues differe solmen in li grammatica, li pronunciation e li plu commun vocabules. Omnicos directe al desirabilite de un nov lingua franca: On refusa',
status: 0,
hide: false,
id: 14,
},
{
title: 'Learn Javascript',
description:
'The European languages are members of the same family. Their separate existence is a myth. For science, music, sport, etc, Europe uses the same vocabulary. The languages only differ in their grammar, their pronunciation and their most common words. Everyone realizes why a new common language would be desirable: one',
status: 0,
hide: false,
id: 15,
},
]);
}, []);
function handleHideTodos() {
const newHideTodos = !hideTodos;
setHideTodos(newHideTodos);
if (newHideTodos) {
const newTodos = todos.map((todo, index) => {
if (index >= maxDisplayTodos) todo.hide = false;
return todo;
});
setTodos(newTodos);
} else {
const newTodos = todos.map((todo, index) => {
if (index >= maxDisplayTodos) todo.hide = true;
return todo;
});
setTodos(newTodos);
}
}
return (
<section className="ToDoApp">
<h1>ToDo App</h1>
<div className="ToDoList">
{(todos || []).map((todo, index) => (
<ToDoCard
key={index}
{...todo}
/>
))}
{(!todos || todos.length === 0) && (
<div className="ToDoList__empty">
<p>No todos found</p>
</div>
)}
{todos.length > maxDisplayTodos && (
<button className="ToDoList__action" type="button" onClick={() => handleHideTodos()}>
{hideTodos ? 'HIDE' : 'SHOW'}
</button>
)}
</div>
</section>
);
}
There is another state variable hideTodos used to determine when to hide the todos and when not to. Also there is a function handleHideTodos() that handles the state variable hideTodos and based on the current status of hideTodos we either hide or show off the maxDisplayTodos limit. We also have a no todos found for no todos and a togglable show/hide button based on hideTodos.
Form Component
Before we start on with the add, edit and delete of todos, let's introduce our form component.
ToDoForm.jsx
import React from 'react';
import { FaTimes } from 'react-icons/fa';
function ToDoForm({
title: titleProps,
description: descriptionProps,
status: statusProps,
id,
}) {
const [title, setTitle] = React.useState(titleProps);
const [description, setDescription] = React.useState(descriptionProps);
const [status, setStatus] = React.useState(statusProps);
function handleTitleChange(e) {
setTitle(e.target.value);
}
function handleDescriptionChange(e) {
setDescription(e.target.value);
}
function handleStatusChange(e) {
setStatus(parseInt(e.target.value));
}
return (
<form className="ToDoForm">
<FaTimes className="close-btn"/>
<h2>ToDo Form</h2>
<div className="ToDoForm__field">
<label htmlFor="title">Title</label>
<input type="text" id="title" value={title} onChange={(e) => handleTitleChange(e)} />
</div>
<div className="ToDoForm__field">
<label htmlFor="description">Description</label>
<textarea
type="text"
id="description"
value={description}
onChange={(e) => handleDescriptionChange(e)}
/>
</div>
<div className="ToDoForm__field">
<label htmlFor="status">Status</label>
<select id="status" value={status} onChange={(e) => handleStatusChange(e)}>
<option value="0">Pending</option>
<option value="1">Working</option>
<option value="2">Done</option>
</select>
</div>
<div className="ToDoForm__action">
<button type="submit">{id === -1 ? 'Add' : 'Update'}</button>
</div>
</form>
);
}
ToDoForm.defaultProps = {
title: '',
description: '',
status: 0,
id: -1,
};
export default ToDoForm;
Handling form elements poses a trouble in React if being handled with state variables, we need to handle inputChange with event handler. So there are three state variables (title, description and status) and three inputChange handlers (handleTitleChange, handleDescriptionChange, handleStatusChange).
Styling ToDoForm Component
ToDoApp.css
...
/* @ToDo Form Layout */
.ToDoForm {
padding: 0.5rem;
border: 1px solid var(--black);
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: space-around;
position: relative;
}
.ToDoForm .close-btn {
position: absolute;
right: 0.5rem;
top: 0.5rem;
}
.ToDoForm__field,
.ToDoForm__action {
display: flex;
align-items: center;
flex-direction: row;
gap: 0.5rem;
}
.ToDoForm__field label {
flex: 0 0 6rem;
font-size: 1rem;
}
.ToDoForm__action button {
margin-left: auto;
}
Adding Form Component & Close Form
ToDoApp.jsx
...
export default function ToDoApp(){
...
const [showForm, setShowForm] = React.useState(false);
...
return (
<section className="ToDoApp">
...
{showForm && (
<ToDoForm
closeForm={() => {
setShowForm(false);
}}
/>
)}
</section>
);
}
Added a showForm state variable, pass it to the form component.
ToDoForm.jsx
...
function ToDoForm({
title: titleProps,
description: descriptionProps,
status: statusProps,
id,
closeForm,
)} {
...
function handleCloseForm() {
setTitle('');
setDescription('');
setStatus(0);
closeForm();
}
return (
<form className="ToDoForm">
<FaTimes className="close-btn" onClick={() => handleCloseForm()} />
...
</form>
);
}
...
Adding a handler for closeform with setting all state variables to initial state.
Searching Todo Items
ToDoApp.jsx
...
export default function ToDoApp() {
...
const [searchText, setSearchText] = React.useState('');
...
function handleSearchChange(evt) {
setSearchText(evt.target.value);
const newTodos = todos.map((todo) => {
todo.hide = !(
todo.title.toLowerCase().includes(evt.target.value.toLowerCase()) ||
todo.description.toLowerCase().includes(evt.target.value.toLowerCase())
);
return todo;
});
setTodos(newTodos);
}
return (
<section className="ToDoApp">
<h1>ToDo App</h1>
<div className="ToDoApp__Search">
<input
type="text"
value={searchText}
onChange={(evt) => handleSearchChange(evt)}
placeholder="Search"
/>
<button className="ToDoApp__create_btn">
<FaPlusCircle />
</button>
</div>
...
</section>
);
}
Used a state variable searchText for storing search input value, also handled the search change with hiding the list that didn't match the search. In case of long list, might have queried it from a database with a loader.
Add Todo Items
ToDoApp.jsx
...
export default function ToDoApp() {
...
function handleAddTodo(todo) {
const newTodo = {
title: todo.title,
description: todo.description,
status: 0,
hide: false,
id: Date.now() % 1000000,
};
setTodos([...todos, newTodo]);
setShowForm(false);
}
...
return (
<section className="ToDoApp">
<h1>ToDo App</h1>
<div className="ToDoApp__Search">
...
<button className="ToDoApp__create_btn" onClick={() => setShowForm(true)}>
<FaPlusCircle />
</button>
</div>
{showForm && (
<ToDoForm
handleAddTodo={handleAddTodo}
closeForm={() => {
setShowForm(false);
}}
/>
)}
...
</section>
);
}
Defining a handleAddToDo handler function, to add a new ToDo object to the ToDos and maintaining closing form on submit. Opening form on click of create Todo button.
ToDoForm.jsx
...
function ToDoForm({
title: titleProps,
description: descriptionProps,
status: statusProps,
id,
closeForm,
handleAddTodo,
}) {
...
function handleFormSubmit(e) {
e.preventDefault();
if (title === '' || description === '') {
alert('Please fill in all fields');
return;
}
handleAddTodo({ title, description, status });
setTitle('');
setDescription('');
setStatus(0);
}
return (
<form className="ToDoForm" onSubmit={(e) => handleFormSubmit(e)}>
...
</form>
);
}
...
Defining handleFormSubmit function to set to initial values and trigger addtodo handler.
Edit Todo Item
Editing Todo Item is little bit tricky, because we need remember the id of the element to be edit with its value passed onto the todo form. Let's see how that happens.
ToDoApp.jsx
...
export default function ToDoApp() {
const [currentTodo, setCurrentTodo] = React.useState({});
...
function handleEditTodo(id) {
setShowForm(true);
const todo = todos.find((todo) => todo.id === id);
setCurrentTodo(todo);
}
function handleAddTodo(todo) {
if (todo.id === undefined) {
const newTodo = {
title: todo.title,
description: todo.description,
status: 0,
hide: false,
id: Date.now() % 1000000,
};
setTodos([...todos, newTodo]);
} else {
const newTodos = todos.map((todo_) => {
if (todo.id === todo_.id) {
todo_.title = todo.title;
todo_.description = todo.description;
todo_.status = todo.status;
}
return todo_;
});
setTodos(newTodos);
}
setCurrentTodo({});
setShowForm(false);
}
return (
<section className="ToDoApp">
...
{showForm && (
<ToDoForm
handleAddTodo={handleAddTodo}
{...currentTodo}
closeForm={() => {
setCurrentTodo({});
setShowForm(false);
}}
/>
)}
<div className="ToDoList">
{(todos || []).map((todo, index) => (
<ToDoCard
key={index}
{...todo}
handleEditTodo={handleEditTodo}
/>
))}
...
</div>
</section>
);
}
Adding a state variable currentTodo to set the current Todo object for edit and passing as prop to the ToDo Form and also modifying handleAddTodo function to update already existing Todo object. Add handleEditTodo function to set currentTodo for current element.
ToDoForm.jsx
...
function ToDoForm({
title: titleProps,
description: descriptionProps,
status: statusProps,
id,
closeForm,
handleAddTodo,
}) {
...
function handleFormSubmit(e) {
e.preventDefault();
if (title === '' || description === '') {
alert('Please fill in all fields');
return;
}
if (id >= 0) handleAddTodo({ title, description, status, id: id });
else handleAddTodo({ title, description, status });
setTitle('');
setDescription('');
setStatus(0);
}
...
}
...
Modifying handleFormSubmit function to handle both create and update cases.
ToDoCard.jsx
...
export default function ToDoCard({
id,
title,
description,
status,
hide,
handleEditTodo,
...otherProps
}){
...
return (
<div className="ToDoCard" {...otherProps}>
...
<div className="ToDoCard__right">
...
<FaPencilAlt
className="ToDoCard__icon"
onClick={() => {
handleEditTodo(id);
}}
/>
</div>
</div>
);
}
Triggering handleEditTodo function for current ToDo element.
Delete ToDo
ToDoApp.jsx
...
export default function ToDoApp() {
...
function handleDeleteTodo(id) {
const newTodos = todos.filter((todo) => todo.id !== id);
setTodos(newTodos);
}
return (
<section className="ToDoApp">
...
<div className="ToDoList">
{(todos || []).map((todo, index) => (
<ToDoCard
key={index}
{...todo}
handleEditTodo={handleEditTodo}
handleDeleteTodo={handleDeleteTodo}
/>
))}
...
</div>
</section>
);
}
Creating a handleDeleteTodo function for an id, updating the todos without the given id todo object and pass on to ToDoCard.
ToDoCard.jsx
...
export default function ToDoCard({
id,
title,
description,
status,
hide,
handleEditTodo,
handleDeleteTodo,
...otherProps
}){
...
return (
<div className="ToDoCard" {...otherProps}>
...
<div className="ToDoCard__right">
<FaTimesCircle
className="ToDoCard__icon red_text"
onClick={() => {
handleDeleteTodo(id);
}}
/>
...
</div>
</div>
);
}
...
ToDoCard element on click of delete button trigger handleDeleteTodo for current element id.
Change Status
ToDoApp.jsx
...
export default function ToDoApp() {
...
function handleChangeStatus(id) {
const newTodos = todos.map((todo) => {
if (todo.id === id) {
todo.status = todo.status === 2 ? 0 : todo.status + 1;
}
return todo;
});
setTodos(newTodos);
}
return (
<section className="ToDoApp">
...
<div className="ToDoList">
{(todos || []).map((todo, index) => (
<ToDoCard
key={index}
{...todo}
handleChangeStatus={handleChangeStatus}
handleEditTodo={handleEditTodo}
handleDeleteTodo={handleDeleteTodo}
/>
))}
...
</div>
</section>
);
}
Added a handler for changestatus for id and is passed to ToDoCard for invokation. The handler updates the last status from 0 to 2 and back to 0 in a circular fashion.
ToDoCard.jsx
...
export default function ToDoCard({
id,
title,
description,
status,
hide,
handleEditTodo,
handleDeleteTodo,
handleChangeStatus,
...otherProps
}) {
...
return (
<div className="ToDoCard" {...otherProps}>
<div className="ToDoCard__left">
<span
onClick={() => {
handleChangeStatus(id);
}}
>
{status === 0 && <FaExclamationCircle title="Pending" className="ToDoCard__icon grey_text" />}
{status === 1 && <FaClock title="Working" className="ToDoCard__icon blue_text" />}
{status === 2 && <FaCheckCircle title="Done" className="ToDoCard__icon green_text" />}
</span>
</div>
...
</div>
);
}
Passed function for status change is onclick triggered for status icon that is changed with varied status value.
Final Code
ToDoApp.css
https://github.com/shivishbrahma/nuclear-reactor/blob/main/src/ToDoApp/ToDoApp.css
ToDoApp.jsx
https://github.com/shivishbrahma/nuclear-reactor/blob/main/src/ToDoApp/ToDoApp.jsx
ToDoCard.jsx
https://github.com/shivishbrahma/nuclear-reactor/blob/main/src/ToDoApp/ToDoCard.jsx
ToDoForm.jsx
https://github.com/shivishbrahma/nuclear-reactor/blob/main/src/ToDoApp/ToDoForm.jsx
Top comments (0)