Create A ToDo List Component Using ReactJS

Hello Guys,

In this article, we will learn about how to create a TODO list using react.

This is an example of a challenge you could face when applying for front-end developer jobs.

You’ve been given a set of functional requirements and instructed to create a todo component. Try it on your own and refer to my sample demo and code if you get stuck. Set a timer for 45 minutes and try explaining your thought process out loud to replicate an actual interview situation.

Instructions:

  • Make a to-do list component.
  • An initial list of todos should be accepted by the component.
  • Add button and input for adding new todos to the list.
  • When todo is clicked, its state (complete/incomplete) should be updated.
    • Todos that have been completed should have strikethrough text style and checkmark icon.
    • Todos that aren’t completed should have hollow circle icon next to them.
  • Todos should include delete button that allows the user to remove the item from the list.
  • Bonus points:
    • Toggle between all todos, finished todos, and incomplete todos by adding tabs to the top of the todo list.
    • Add section to show the percentage of todos that have been done out of the overall amount of todos.
    • Update the area to provide success message once all todos have been performed.
    • Both light and dark mode styles are supported.

For the component, here are some icons to use:

Checkmark Icon:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor">
    <path d="M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM371.8 211.8C382.7 200.9 382.7 183.1 371.8 172.2C360.9 161.3 343.1 161.3 332.2 172.2L224 280.4L179.8 236.2C168.9 225.3 151.1 225.3 140.2 236.2C129.3 247.1 129.3 264.9 140.2 275.8L204.2 339.8C215.1 350.7 232.9 350.7 243.8 339.8L371.8 211.8z" />
</svg>

Unchecked Icon:

<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
    <path d="M512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256zM256 48C141.1 48 48 141.1 48 256C48 370.9 141.1 464 256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48z" />
</svg>

Trashcan Icon:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="currentColor">
    <path d="M135.2 17.69C140.6 6.848 151.7 0 163.8 0H284.2C296.3 0 307.4 6.848 312.8 17.69L320 32H416C433.7 32 448 46.33 448 64C448 81.67 433.7 96 416 96H32C14.33 96 0 81.67 0 64C0 46.33 14.33 32 32 32H128L135.2 17.69zM394.8 466.1C393.2 492.3 372.3 512 346.9 512H101.1C75.75 512 54.77 492.3 53.19 466.1L31.1 128H416L394.8 466.1z" />
</svg>

App.js:

import "./styles.css";
import { useState } from "react";
import { TrashCanIcon, CheckIcon, UncheckedIcon } from "./Icons";

const initialData = [
  { id: 1, title: "Practice Leetcode", completed: false },
  { id: 2, title: "Write blog post", completed: false },
  { id: 3, title: "Go to grocery store", completed: true },
  { id: 4, title: "Walk the dog", completed: false }
];

const StatusIcon = ({ completed }) => {
  return completed ? (
    <CheckIcon className="status-icon completed" />
  ) : (
    <UncheckedIcon className="status-icon" />
  );
};

const ToDoItem = ({ item, handleStatus, handleRemove }) => {
  const { title, completed } = item;
  return (
    <div className="todo">
      <button
        className="todo-item"
        style={{
          textDecoration: completed ? `line-through` : `none`
        }}
        onClick={handleStatus}
      >
        <StatusIcon completed={completed} />
        {title}
      </button>
      <button className="btn-delete" onClick={handleRemove}>
        <TrashCanIcon className="delete-icon" />
      </button>
    </div>
  );
};

const FilterButton = ({ activeFilter, filter, onClick }) => {
  return (
    <button
      className={`filter ${activeFilter === filter && `active`}`}
      onClick={onClick}
    >
      {filter}
    </button>
  );
};

const ToDo = () => {
  const [addTodo, setAddTodo] = useState("");
  const [filter, setFilter] = useState("all");
  const [todos, setTodos] = useState(initialData);

  const handleStatus = (item) => {
    const itemIndex = todos.findIndex((elem) => elem.id === item.id);
    const updatedTodo = todos[itemIndex];
    const newTodos = [...todos];
    newTodos.splice(itemIndex, 1, {
      ...updatedTodo,
      completed: !updatedTodo.completed
    });
    setTodos(newTodos);
  };

  const handleRemove = (item) => {
    const itemIndex = todos.findIndex((elem) => elem.id === item.id);
    const newTodos = [...todos];
    newTodos.splice(itemIndex, 1);
    setTodos(newTodos);
  };

  const handleAddTodo = () => {
    const updatedTodos = [...todos];
    updatedTodos.push({ title: addTodo, completed: false, id: Date.now() });
    setTodos(updatedTodos);
    setAddTodo("");
  };

  const filterTodos = () => {
    if (filter === "complete") {
      return todos.filter((item) => item.completed === true);
    } else if (filter === "incomplete") {
      return todos.filter((item) => item.completed === false);
    } else {
      return todos;
    }
  };

  const handleChange = (e) => {
    setAddTodo(e.target.value);
  };

  const visibleTodos = filterTodos();
  const doneCount = todos.filter((item) => item.completed).length;

  return (
    <div>
      <div className="field">
        <input value={addTodo} onChange={handleChange} />
        <button
          className="btn btn--add"
          onClick={handleAddTodo}
          disabled={addTodo.length < 1}
        >
          Add
        </button>
      </div>
      <div className="filter-wrapper">
        <div className="filter-tabs">
          <FilterButton
            activeFilter={filter}
            filter="all"
            onClick={() => setFilter("all")}
          />
          <FilterButton
            activeFilter={filter}
            filter="complete"
            onClick={() => setFilter("complete")}
          />
          <FilterButton
            activeFilter={filter}
            filter="incomplete"
            onClick={() => setFilter("incomplete")}
          />
        </div>
        <p style={{ lineHeight: 1.5 }}>
          {doneCount === todos.length
            ? `🎉 ${doneCount}/${todos.length} all todos complete!`
            : `${doneCount}/${todos.length} todos complete`}
        </p>
      </div>
      {visibleTodos.length === 0 && (
        <p style={{ paddingLeft: "1rem" }}>No todos to show here...</p>
      )}
      {visibleTodos.length > 0 &&
        visibleTodos.map((item, idx) => {
          return (
            <ToDoItem
              key={item.id}
              item={item}
              handleStatus={() => handleStatus(item)}
              handleRemove={() => handleRemove(item)}
            />
          );
        })}
    </div>
  );
};

function App() {
  return (
    <div className="container">
      <h1>
        <strong>ToDo</strong> Component
      </h1>
      <ToDo />
    </div>
  );
}
export default App;

Icons.js:

export const CheckIcon = ({ className }) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 512 512"
    fill="currentColor"
    className={className}
  >
    <path d="M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM371.8 211.8C382.7 200.9 382.7 183.1 371.8 172.2C360.9 161.3 343.1 161.3 332.2 172.2L224 280.4L179.8 236.2C168.9 225.3 151.1 225.3 140.2 236.2C129.3 247.1 129.3 264.9 140.2 275.8L204.2 339.8C215.1 350.7 232.9 350.7 243.8 339.8L371.8 211.8z" />
  </svg>
);

export const UncheckedIcon = ({ className }) => (
  <svg
    fill="currentColor"
    xmlns="http://www.w3.org/2000/svg"
    className={className}
    viewBox="0 0 512 512"
  >
    <path d="M512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256zM256 48C141.1 48 48 141.1 48 256C48 370.9 141.1 464 256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48z" />
  </svg>
);

export const TrashCanIcon = ({ className }) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 448 512"
    className={className}
    fill="currentColor"
  >
    <path d="M135.2 17.69C140.6 6.848 151.7 0 163.8 0H284.2C296.3 0 307.4 6.848 312.8 17.69L320 32H416C433.7 32 448 46.33 448 64C448 81.67 433.7 96 416 96H32C14.33 96 0 81.67 0 64C0 46.33 14.33 32 32 32H128L135.2 17.69zM394.8 466.1C393.2 492.3 372.3 512 346.9 512H101.1C75.75 512 54.77 492.3 53.19 466.1L31.1 128H416L394.8 466.1z" />
  </svg>
);

Output:

Submit a Comment

Your email address will not be published.

Subscribe

Select Categories