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 a button and input for adding new todos to the list.
- When a todo is clicked, its state (complete/incomplete) should be updated.
- Todos that have been completed should have a strikethrough text style and a checkmark icon.
- Todos that aren’t completed should have a hollow circle icon next to them.
- Todos should include a 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 a section to show the percentage of todos that have been done out of the overall amount of todos.
-
Update the area to provide a 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: