DEV Community

Cover image for Building a ToDo App in React
Purbayan Chowdhury
Purbayan Chowdhury

Posted on

Building a ToDo App in React

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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%;
}
Enter fullscreen mode Exit fullscreen mode

Card Component

Before we get started, let's install react-icons by executing

npm i react-icons
Enter fullscreen mode Exit fullscreen mode

Defining the json schema for each todo

{
    "title": "string",
    "description": "string",
    "status": "integer(0,1,2)",
    "hide": "boolean",
    "id": "integer"
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
    );

}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
...
Enter fullscreen mode Exit fullscreen mode

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>

    );
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}

Enter fullscreen mode Exit fullscreen mode

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>
    );
}
...
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
    ...
}
...
Enter fullscreen mode Exit fullscreen mode

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>
    );
}

Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
...

Enter fullscreen mode Exit fullscreen mode

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>
    );  
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}

Enter fullscreen mode Exit fullscreen mode

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

Preview

Preview of TodoApp

Top comments (0)