Skip to content
react

How to build To-do list in React

Feb 15, 2022Abhishek EH10 Min Read
How to build To-do list in React

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:

index.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}
16
17.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:

App.js
1import {
2 Button,
3 Card,
4 ControlGroup,
5 Elevation,
6 InputGroup,
7} from "@blueprintjs/core"
8import { useState } from "react"
9
10function App() {
11 const [userInput, setUserInput] = useState("")
12
13 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 <InputGroup
20 placeholder="Add a task..."
21 value={userInput}
22 onChange={e => setUserInput(e.target.value)}
23 />
24 <Button type="submit" intent="primary">
25 Add
26 </Button>
27 </ControlGroup>
28 </form>
29 </Card>
30 </div>
31 )
32}
33
34export 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:

App.js
1import {
2 Button,
3 Card,
4 ControlGroup,
5 Elevation,
6 InputGroup,
7} from "@blueprintjs/core"
8import { useState } from "react"
9
10function App() {
11 const [userInput, setUserInput] = useState("")
12
13 const [todoList, setTodoList] = useState([])
14
15 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 }
26
27 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 <InputGroup
34 placeholder="Add a task..."
35 value={userInput}
36 onChange={e => setUserInput(e.target.value)}
37 />
38 <Button type="submit" intent="primary">
39 Add
40 </Button>
41 </ControlGroup>
42 </form>
43 </Card>
44 </div>
45 )
46}
47
48export 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:

App.js
1import {
2 Button,
3 Card,
4 ControlGroup,
5 Elevation,
6 InputGroup,
7 Tag,
8} from "@blueprintjs/core"
9import { useState } from "react"
10
11function App() {
12 const [userInput, setUserInput] = useState("")
13
14 const [todoList, setTodoList] = useState([])
15
16 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 }
27
28 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 <InputGroup
35 placeholder="Add a task..."
36 value={userInput}
37 onChange={e => setUserInput(e.target.value)}
38 />
39 <Button type="submit" intent="primary">
40 Add
41 </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}
55
56export default App

Now if we add the tasks, it will be displayed as follows:

todo display tasks

Marking the task as done

Now let's add option to mark the task as done:

App.js
1import {
2 Button,
3 Card,
4 Checkbox,
5 ControlGroup,
6 Elevation,
7 InputGroup,
8 Tag,
9} from "@blueprintjs/core"
10import { useState } from "react"
11
12function App() {
13 const [userInput, setUserInput] = useState("")
14
15 const [todoList, setTodoList] = useState([])
16
17 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 }
28
29 const toggleTask = index => {
30 setTodoList(existingItems =>
31 existingItems.map((item, i) =>
32 index === i ? { ...item, finished: !item.finished } : item
33 )
34 )
35 }
36
37 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 <InputGroup
44 placeholder="Add a task..."
45 value={userInput}
46 onChange={e => setUserInput(e.target.value)}
47 />
48 <Button type="submit" intent="primary">
49 Add
50 </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 <Checkbox
57 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}
71
72export 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:

todo strike tasks

Deleting the tasks

We will also add a feature to delete the tasks which are not needed:

App.js
1import {
2 Button,
3 Card,
4 Checkbox,
5 ControlGroup,
6 Elevation,
7 InputGroup,
8 Tag,
9} from "@blueprintjs/core"
10import { useState } from "react"
11
12function App() {
13 const [userInput, setUserInput] = useState("")
14
15 const [todoList, setTodoList] = useState([])
16
17 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 }
28
29 const toggleTask = index => {
30 setTodoList(existingItems =>
31 existingItems.map((item, i) =>
32 index === i ? { ...item, finished: !item.finished } : item
33 )
34 )
35 }
36
37 const deleteTask = index => {
38 setTodoList(existingItems => existingItems.filter((item, i) => index !== i))
39 }
40
41 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 <InputGroup
48 placeholder="Add a task..."
49 value={userInput}
50 onChange={e => setUserInput(e.target.value)}
51 />
52 <Button type="submit" intent="primary">
53 Add
54 </Button>
55 </ControlGroup>
56 </form>
57 <div className="items-list">
58 {todoList.map((item, index) => (
59 <Tag
60 key={index + item.name}
61 large
62 minimal
63 multiline
64 onRemove={() => deleteTask(index)}
65 >
66 <Checkbox
67 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}
81
82export 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.

todo list with remove icon

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:

useLocalStorage.js
1import { useState } from "react"
2
3const useLocalStorage = (key, initialValue) => {
4 const [state, setState] = useState(() => {
5 // Initialize the state
6 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 initialValue
10 return value ? JSON.parse(value) : initialValue
11 } catch (error) {
12 console.log(error)
13 }
14 })
15
16 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) : value
21 window.localStorage.setItem(key, JSON.stringify(valueToStore))
22 setState(value)
23 } catch (error) {
24 console.log(error)
25 }
26 }
27
28 return [state, setValue]
29}
30
31export default useLocalStorage

Now all we need to do is import it in the App component and use it instead of the useState hook:

App.js
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"
12
13function App() {
14 const [userInput, setUserInput] = useState("")
15
16 const [todoList, setTodoList] = useLocalStorage("todo-items", [])
17
18 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 }
29
30 const toggleTask = index => {
31 setTodoList(existingItems =>
32 existingItems.map((item, i) =>
33 index === i ? { ...item, finished: !item.finished } : item
34 )
35 )
36 }
37
38 const deleteTask = index => {
39 setTodoList(existingItems => existingItems.filter((item, i) => index !== i))
40 }
41
42 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 <InputGroup
49 placeholder="Add a task..."
50 value={userInput}
51 onChange={e => setUserInput(e.target.value)}
52 />
53 <Button type="submit" intent="primary">
54 Add
55 </Button>
56 </ControlGroup>
57 </form>
58 <div className="items-list">
59 {todoList.map((item, index) => (
60 <Tag
61 key={index + item.name}
62 large
63 minimal
64 multiline
65 onRemove={() => deleteTask(index)}
66 >
67 <Checkbox
68 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}
82
83export 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.

todo localStorage

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:

todo.css
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}
13
14.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}
24
25.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:

ToDo.js
1import { useState } from "react"
2import useLocalStorage from "./useLocalStorage"
3import "./todo.css"
4
5function ToDo() {
6 const [userInput, setUserInput] = useState("")
7
8 const [todoList, setTodoList] = useLocalStorage("todo-items", [])
9
10 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 }
21
22 const toggleTask = index => {
23 setTodoList(existingItems =>
24 existingItems.map((item, i) =>
25 index === i ? { ...item, finished: !item.finished } : item
26 )
27 )
28 }
29
30 const deleteTask = index => {
31 setTodoList(existingItems => existingItems.filter((item, i) => index !== i))
32 }
33
34 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 <input
41 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 Add
48 </button>
49 </div>
50 </form>
51 <div className="items-list">
52 {todoList.map((item, index) => (
53 <label
54 className="tag"
55 key={index + item.name}
56 htmlFor={`checkbox-${index + item.name}`}
57 >
58 <input
59 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}
77
78export 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!

© 2024 CodingDeft.Com