Table of Contents
In this article, we will see how to build a to-do list in React and mark a task as done, and delete a task that is not needed. We will also learn how to store the to-do list locally in the browser and restore it when the user reloads the page.
Project setup
Create a react app using the following command:
1npx create-react-app react-todo
We will be making use of BlueprintJS to style our app. So let's go ahead and install it:
1npm i @blueprintjs/core
Now open index.css
and import the Blueprint css:
1@import "~normalize.css";2@import "~@blueprintjs/core/lib/css/blueprint.css";3@import "~@blueprintjs/icons/lib/css/blueprint-icons.css";4body {5 margin: 10px auto;6 max-width: 400px;7}8.heading {9 text-align: center;10}11.items-list {12 margin-top: 10px;13 display: flex;14 flex-direction: column;15}1617.items-list .bp3-tag {18 margin: 8px 0;19}20.items-list .bp3-checkbox {21 margin-left: 4px;22 margin-bottom: 4px;23}24.finished {25 text-decoration: line-through;26}
We have also added some styles which we will be using in the next sections.
Creating form input
Since we need a text input to type the task, let's add it to App.js
:
1import {2 Button,3 Card,4 ControlGroup,5 Elevation,6 InputGroup,7} from "@blueprintjs/core"8import { useState } from "react"910function App() {11 const [userInput, setUserInput] = useState("")1213 return (14 <div className="App">15 <Card elevation={Elevation.TWO}>16 <h2 className="heading">To-do List</h2>17 <form>18 <ControlGroup fill={true} vertical={false}>19 <InputGroup20 placeholder="Add a task..."21 value={userInput}22 onChange={e => setUserInput(e.target.value)}23 />24 <Button type="submit" intent="primary">25 Add26 </Button>27 </ControlGroup>28 </form>29 </Card>30 </div>31 )32}3334export default App
Here the local state, userInput
stores the text in the input box.
Storing the to-do items in local state
Now let's have another state to store the list of to-do items and update it whenever user submits the form:
1import {2 Button,3 Card,4 ControlGroup,5 Elevation,6 InputGroup,7} from "@blueprintjs/core"8import { useState } from "react"910function App() {11 const [userInput, setUserInput] = useState("")1213 const [todoList, setTodoList] = useState([])1415 const addItem = e => {16 e.preventDefault()17 const trimmedUserInput = userInput.trim()18 if (trimmedUserInput) {19 setTodoList(existingItems => [20 ...existingItems,21 { name: trimmedUserInput, finished: false },22 ])23 setUserInput("")24 }25 }2627 return (28 <div className="App">29 <Card elevation={Elevation.TWO}>30 <h2 className="heading">To-do List</h2>31 <form onSubmit={addItem}>32 <ControlGroup fill={true} vertical={false}>33 <InputGroup34 placeholder="Add a task..."35 value={userInput}36 onChange={e => setUserInput(e.target.value)}37 />38 <Button type="submit" intent="primary">39 Add40 </Button>41 </ControlGroup>42 </form>43 </Card>44 </div>45 )46}4748export default App
Along with the name of the task we are storing a boolean flag called finished
, which will store the status of the task.
Displaying the list of tasks
As of now, we are not displaying the tasks. Let's go ahead and do it:
1import {2 Button,3 Card,4 ControlGroup,5 Elevation,6 InputGroup,7 Tag,8} from "@blueprintjs/core"9import { useState } from "react"1011function App() {12 const [userInput, setUserInput] = useState("")1314 const [todoList, setTodoList] = useState([])1516 const addItem = e => {17 e.preventDefault()18 const trimmedUserInput = userInput.trim()19 if (trimmedUserInput) {20 setTodoList(existingItems => [21 ...existingItems,22 { name: trimmedUserInput, finished: false },23 ])24 setUserInput("")25 }26 }2728 return (29 <div className="App">30 <Card elevation={Elevation.TWO}>31 <h2 className="heading">To-do List</h2>32 <form onSubmit={addItem}>33 <ControlGroup fill={true} vertical={false}>34 <InputGroup35 placeholder="Add a task..."36 value={userInput}37 onChange={e => setUserInput(e.target.value)}38 />39 <Button type="submit" intent="primary">40 Add41 </Button>42 </ControlGroup>43 </form>44 <div className="items-list">45 {todoList.map((item, index) => (46 <Tag key={index + item.name} large minimal multiline>47 <span>{item.name}</span>48 </Tag>49 ))}50 </div>51 </Card>52 </div>53 )54}5556export default App
Now if we add the tasks, it will be displayed as follows:
Marking the task as done
Now let's add option to mark the task as done:
1import {2 Button,3 Card,4 Checkbox,5 ControlGroup,6 Elevation,7 InputGroup,8 Tag,9} from "@blueprintjs/core"10import { useState } from "react"1112function App() {13 const [userInput, setUserInput] = useState("")1415 const [todoList, setTodoList] = useState([])1617 const addItem = e => {18 e.preventDefault()19 const trimmedUserInput = userInput.trim()20 if (trimmedUserInput) {21 setTodoList(existingItems => [22 ...existingItems,23 { name: trimmedUserInput, finished: false },24 ])25 setUserInput("")26 }27 }2829 const toggleTask = index => {30 setTodoList(existingItems =>31 existingItems.map((item, i) =>32 index === i ? { ...item, finished: !item.finished } : item33 )34 )35 }3637 return (38 <div className="App">39 <Card elevation={Elevation.TWO}>40 <h2 className="heading">To-do List</h2>41 <form onSubmit={addItem}>42 <ControlGroup fill={true} vertical={false}>43 <InputGroup44 placeholder="Add a task..."45 value={userInput}46 onChange={e => setUserInput(e.target.value)}47 />48 <Button type="submit" intent="primary">49 Add50 </Button>51 </ControlGroup>52 </form>53 <div className="items-list">54 {todoList.map((item, index) => (55 <Tag key={index + item.name} large minimal multiline>56 <Checkbox57 checked={item.finished}58 onChange={() => toggleTask(index)}59 >60 <span className={item.finished ? "finished" : ""}>61 {item.name}62 </span>63 </Checkbox>{" "}64 </Tag>65 ))}66 </div>67 </Card>68 </div>69 )70}7172export default App
Here we have added a checkbox, which can be checked when the task is complete. We are modifying the array of objects. I have explained in detail how to update an array in useState in my previous article.
Also, we are adding the class 'finished' to the span containing the task so that the finished task will have a strike through as shown below:
Deleting the tasks
We will also add a feature to delete the tasks which are not needed:
1import {2 Button,3 Card,4 Checkbox,5 ControlGroup,6 Elevation,7 InputGroup,8 Tag,9} from "@blueprintjs/core"10import { useState } from "react"1112function App() {13 const [userInput, setUserInput] = useState("")1415 const [todoList, setTodoList] = useState([])1617 const addItem = e => {18 e.preventDefault()19 const trimmedUserInput = userInput.trim()20 if (trimmedUserInput) {21 setTodoList(existingItems => [22 ...existingItems,23 { name: trimmedUserInput, finished: false },24 ])25 setUserInput("")26 }27 }2829 const toggleTask = index => {30 setTodoList(existingItems =>31 existingItems.map((item, i) =>32 index === i ? { ...item, finished: !item.finished } : item33 )34 )35 }3637 const deleteTask = index => {38 setTodoList(existingItems => existingItems.filter((item, i) => index !== i))39 }4041 return (42 <div className="App">43 <Card elevation={Elevation.TWO}>44 <h2 className="heading">To-do List</h2>45 <form onSubmit={addItem}>46 <ControlGroup fill={true} vertical={false}>47 <InputGroup48 placeholder="Add a task..."49 value={userInput}50 onChange={e => setUserInput(e.target.value)}51 />52 <Button type="submit" intent="primary">53 Add54 </Button>55 </ControlGroup>56 </form>57 <div className="items-list">58 {todoList.map((item, index) => (59 <Tag60 key={index + item.name}61 large62 minimal63 multiline64 onRemove={() => deleteTask(index)}65 >66 <Checkbox67 checked={item.finished}68 onChange={() => toggleTask(index)}69 >70 <span className={item.finished ? "finished" : ""}>71 {item.name}72 </span>73 </Checkbox>74 </Tag>75 ))}76 </div>77 </Card>78 </div>79 )80}8182export default App
Here we have supplied the onRemove
handler to the Tag
component of BlueprintJS.
Whenever we supply an onRemove
to the Tag
component, a remove icon will appear and when clicked, it will remove that particular task.
Storing the tasks in local storage
In one of my previous articles, I have explained how to use local storage in React
and we have built a custom useLocalStorage
hook. We will be using the same hook in here.
So let's create useLocalStorage.js in the root directory:
1import { useState } from "react"23const useLocalStorage = (key, initialValue) => {4 const [state, setState] = useState(() => {5 // Initialize the state6 try {7 const value = window.localStorage.getItem(key)8 // Check if the local storage already has any values,9 // otherwise initialize it with the passed initialValue10 return value ? JSON.parse(value) : initialValue11 } catch (error) {12 console.log(error)13 }14 })1516 const setValue = value => {17 try {18 // If the passed value is a callback function,19 // then call it with the existing state.20 const valueToStore = value instanceof Function ? value(state) : value21 window.localStorage.setItem(key, JSON.stringify(valueToStore))22 setState(value)23 } catch (error) {24 console.log(error)25 }26 }2728 return [state, setValue]29}3031export default useLocalStorage
Now all we need to do is import it in the App component and use it instead of the useState hook:
1import {2 Button,3 Card,4 Checkbox,5 ControlGroup,6 Elevation,7 InputGroup,8 Tag,9} from "@blueprintjs/core"10import { useState } from "react"11import useLocalStorage from "./useLocalStorage"1213function App() {14 const [userInput, setUserInput] = useState("")1516 const [todoList, setTodoList] = useLocalStorage("todo-items", [])1718 const addItem = e => {19 e.preventDefault()20 const trimmedUserInput = userInput.trim()21 if (trimmedUserInput) {22 setTodoList(existingItems => [23 ...existingItems,24 { name: trimmedUserInput, finished: false },25 ])26 setUserInput("")27 }28 }2930 const toggleTask = index => {31 setTodoList(existingItems =>32 existingItems.map((item, i) =>33 index === i ? { ...item, finished: !item.finished } : item34 )35 )36 }3738 const deleteTask = index => {39 setTodoList(existingItems => existingItems.filter((item, i) => index !== i))40 }4142 return (43 <div className="App">44 <Card elevation={Elevation.TWO}>45 <h2 className="heading">To-do List</h2>46 <form onSubmit={addItem}>47 <ControlGroup fill={true} vertical={false}>48 <InputGroup49 placeholder="Add a task..."50 value={userInput}51 onChange={e => setUserInput(e.target.value)}52 />53 <Button type="submit" intent="primary">54 Add55 </Button>56 </ControlGroup>57 </form>58 <div className="items-list">59 {todoList.map((item, index) => (60 <Tag61 key={index + item.name}62 large63 minimal64 multiline65 onRemove={() => deleteTask(index)}66 >67 <Checkbox68 checked={item.finished}69 onChange={() => toggleTask(index)}70 >71 <span className={item.finished ? "finished" : ""}>72 {item.name}73 </span>74 </Checkbox>75 </Tag>76 ))}77 </div>78 </Card>79 </div>80 )81}8283export default App
Now if you check the local storage of the browser, you will see that the items along with the finish status are stored there.
Implementing with pure css
If you don't wish to use BlueprintJS, you can see the implementation below with css and HTML elements:
First create todo.css
file:
1body {2 margin: 10px auto;3 max-width: 400px;4}5.heading {6 text-align: center;7}8.items-list {9 margin-top: 10px;10 display: flex;11 flex-direction: column;12}1314.finished {15 text-decoration: line-through;16}17.card {18 background-color: #fff;19 border-radius: 3px;20 padding: 20px;21 box-shadow: 0 0 0 1px rgb(16 22 26 / 10%), 0 1px 1px rgb(16 22 26 / 20%),22 0 2px 6px rgb(16 22 26 / 20%);23}2425.input-wrapper {26 display: flex;27}28.input {29 flex-grow: 1;30 border-right: none;31 background: #fff;32 border: none;33 border-radius: 3px 0 0 3px;34 box-shadow: 0 0 0 0 rgb(19 124 189 / 0%), 0 0 0 0 rgb(19 124 189 / 0%),35 inset 0 0 0 1px rgb(16 22 26 / 15%), inset 0 1px 1px rgb(16 22 26 / 20%);36 color: #182026;37 font-size: 14px;38 font-weight: 400;39 height: 30px;40 line-height: 30px;41 outline: none;42 padding: 0 10px;43}44.input:focus {45 box-shadow: 0 0 0 1px #137cbd, 0 0 0 3px rgb(19 124 189 / 30%),46 inset 0 1px 1px rgb(16 22 26 / 20%);47}48.add-btn {49 width: 100px;50 background-color: #137cbd;51 background-image: linear-gradient(52 180deg,53 hsla(0, 0%, 100%, 0.1),54 hsla(0, 0%, 100%, 0)55 );56 box-shadow: inset 0 0 0 1px rgb(16 22 26 / 40%), inset 0 -1px 0 rgb(16 22 26 /57 20%);58 color: #fff;59 border-radius: 0 3px 3px 0;60 cursor: pointer;61 border: none;62 padding: 5px 10px;63}64.add-btn:focus {65 outline: 2px auto rgba(19, 124, 189, 0.6);66 outline-offset: 2px;67}68.tag {69 background-color: rgba(138, 155, 168, 0.2);70 color: #182026;71 margin: 8px 0;72 min-height: 30px;73 min-width: 30px;74 padding: 5px 10px;75 border-radius: 3px;76 display: flex;77 align-items: center;78 display: flex;79 cursor: pointer;80}81.label {82 margin-left: 4px;83 flex-grow: 1;84}85.remove-btn {86 border: none;87 background-color: transparent;88 font-size: 24px;89 cursor: pointer;90}91.remove-btn span {92 display: inline-block;93 transform: rotateY(0deg) rotate(45deg);94 opacity: 0.5;95}
Use the css in the ToDo
component:
1import { useState } from "react"2import useLocalStorage from "./useLocalStorage"3import "./todo.css"45function ToDo() {6 const [userInput, setUserInput] = useState("")78 const [todoList, setTodoList] = useLocalStorage("todo-items", [])910 const addItem = e => {11 e.preventDefault()12 const trimmedUserInput = userInput.trim()13 if (trimmedUserInput) {14 setTodoList(existingItems => [15 ...existingItems,16 { name: trimmedUserInput, finished: false },17 ])18 setUserInput("")19 }20 }2122 const toggleTask = index => {23 setTodoList(existingItems =>24 existingItems.map((item, i) =>25 index === i ? { ...item, finished: !item.finished } : item26 )27 )28 }2930 const deleteTask = index => {31 setTodoList(existingItems => existingItems.filter((item, i) => index !== i))32 }3334 return (35 <div className="App">36 <div className="card">37 <h2 className="heading">To-do List</h2>38 <form onSubmit={addItem}>39 <div className="input-wrapper">40 <input41 className="input"42 placeholder="Add a task..."43 value={userInput}44 onChange={e => setUserInput(e.target.value)}45 />46 <button type="submit" className="add-btn">47 Add48 </button>49 </div>50 </form>51 <div className="items-list">52 {todoList.map((item, index) => (53 <label54 className="tag"55 key={index + item.name}56 htmlFor={`checkbox-${index + item.name}`}57 >58 <input59 id={`checkbox-${index + item.name}`}60 type="checkbox"61 checked={item.finished}62 onChange={() => toggleTask(index)}63 />64 <span className={`label ${item.finished ? "finished" : ""}`}>65 {item.name}66 </span>67 <button className="remove-btn" onClick={() => deleteTask(index)}>68 <span>+</span>69 </button>70 </label>71 ))}72 </div>73 </div>74 </div>75 )76}7778export default ToDo
Demo and source code
You can download the source code here and view a demo here.
Do follow me on twitter where I post developer insights more often!