How to create Todo App and how to make data persistence with React

Tomohiro Meo
7 min readFeb 8, 2021

Contents

  • The Target for this article
  • The result of this article
  • How to create Todo App and how to make data persistence with React

The Target for this article

  • People who started to study React recently.
  • People who don’t really know how to use “this” in JavaScript.

The result of this article

  • I use localStorage to make data persistence.

What I create in this tutorial

  • Todo App

1. Create-react-app

You can use create-react-app to build an environment that allows you to develop using React without complex configuration for development and operations.

  • npx create-react-app todo-app

Complete create-react-app

tomohiro@meos-MacBook-Pro learning % npx create-react-app my-appCreating a new React app in /Users/tomohiro/dev/learning/my-app
.
.
.
(omitted)
.
.
yarn remove v1.22.4
[1/2] 🗑 Removing module cra-template...
[2/2] 🔨 Regenerating lockfile and installing missing dependencies...
warning " > @testing-library/user-event@12.6.3" has unmet peer dependency "@testing-library/dom@>=7.21.4".
warning "react-scripts > @typescript-eslint/eslint-plugin > tsutils@3.17.1" has unmet peer dependency "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta".
success Uninstalled packages.
✨ Done in 9.06s.
Created git commit.Success! Created todo-app at /Users/tomohiro/dev/learning/todo-app
Inside that directory, you can run several commands:
yarn start
Starts the development server.
yarn build
Bundles the app into static files for production.
yarn test
Starts the test runner.
yarn eject
Removes this tool and copies build dependencies, configuration files
and scripts into the app directory. If you do this, you can’t go back!
We suggest that you begin by typing:cd todo-app
yarn start
Happy hacking!
tomohiro@meos-MacBook-Pro learning % ls
hello-jest javascript-basic-projects my-app todo-app

Packge.json

{
"name": "todo-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-scripts": "4.0.2",
"web-vitals": "^1.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

2. yarn start

You can start the development server with yarn start.

tomohiro@meos-MacBook-Pro todo-app % yarn start
yarn run v1.22.4
$ react-scripts start
ℹ 「wds」: Project is running at http://10.0.0.112/
ℹ 「wds」: webpack output is served from
ℹ 「wds」: Content not from webpack is served from /Users/tomohiro/dev/learning/todo-app/public
ℹ 「wds」: 404s will fallback to /
Starting the development server...
Browserslist: caniuse-lite is outdated. Please run the following command: `npx browserslist --update-db`
Compiled successfully!
You can now view todo-app in the browser.Local: http://localhost:3000
On Your Network: http://10.0.0.112:3000
Note that the development build is not optimized.
To create a production build, use yarn build.

3. Creating a ToDoListItem component

  • src/App.js
import React, { Component } from 'react';
import './ToDoListItem.css';
class ToDoListItem extends Component {
render() {
const {
title,
description
} = this.props;
return (
<div className="ToDoListItem">
<div className="ToDoListItem-title">{title}</div>
<div className="ToDoListItem-description">{description}</div>
</div>
);
}
}
export default ToDoListItem;

Adding CSS

  • src/ToDoListItem.css
.ToDoListItem {
border: 1px solid aquamarine;
margin: 12px;
border-radius: 4px;
width: 300px;
background-color: #fafbfd;
box-shadow: 1px 2px 5px 3px rgba(0,0,0,.1);
padding: 4px 2px;
}
.ToDoListItem-title {
font-size: 18px;
margin: 0 8px 4px;
border-bottom: 1px solid #333;
text-align: left;
padding: 4px 8px;
}
.ToDoListItem-description {
word-wrap: break-word;
padding: 8px;
}

4. Creating ToDo Entry Form

Modify App.js to add a form.

  • src/App.js
import React, { Component } from 'react';
import './App.css';
import ToDoListItem from "./ToDoListItem.js"
class App extends Component {
render() {
return (
<div className="App">
<form
className="App-form"
>
<div>
<input
id="title"
placeholder="title"
/>
<textarea
id="description"
placeholder="description"
/>
</div>
<div>
<button
type="submit"
>
Register
</button>
</div>
</form>
<div>
<ToDoListItem
title="Test"
description="Test Test"
/>
</div>
</div>
);
}
}
export default App;

Adjusting CSS

  • src/App.css
.App {
width: 800px;
margin: 20px auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.App-form {
display: flex;
flex-direction: column;
width: 300px;
margin: 8px 4px;
padding-bottom: 16px;
border-bottom: 1px solid #aaa;
}
.App-form > :nth-child(1) {
display: flex;
flex-direction: column;
}
.App-form > :nth-child(1) > * {
outline: none;
border: 1px solid #aaa;
transition: all .3s;
border-radius: 2px;
}
.App-form > :nth-child(1) > :first-child {
font-size: 18px;
height: 24px;
padding: 2px 8px;
}
.App-form > :nth-child(1) > :last-child {
margin-top: 4px;
font-size: 16px;
height: 40px;
padding: 2px 8px;
}
.App-form > :nth-child(2) {
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.App-form button {
border: 1px solid #ccc;
background: white;
padding: 4px 8px;
border-radius: 2px;
margin-top: 8px;
cursor: pointer;
box-shadow: 0px 2px 2px 0px rgba(0,0,0,.1);
}
.App-form button:hover {
box-shadow: 0px 2px 2px 2px rgba(0,0,0,.1);
}

5. Create the press button to add todos

You can implement the to add a ToDo using the onSubmit attribute of the form.

  • src/App.js
import React, { Component } from 'react';
import './App.css';
import ToDoListItem from "./ToDoListItem.js"
class App extends Component {// Set state
state = {
todoList: []
}
render() {
return (
<div className="App">
<form
className="App-form"
onSubmit={e => {
e.preventDefault();
// Get the title element
const titleElement = e.target.elements["title"]
const descriptionElement = e.target.elements["description"];
// Add todos to state
this.setState(
{
todoList: this.state.todoList.concat({
title: titleElement.value,
description: descriptionElement.value
})
},
() => {
titleElement.value = "";
descriptionElement.value = "";
}
)
}}
>
<div>
<input
id="title"
placeholder="title"
/>
<textarea
id="description"
placeholder="description"
/>
</div>
<div>
<button
type="submit"
>
Register
</button>
</div>
</form>
<div>
{this.state.todoList.map(todo => (
<ToDoListItem
key={todo.title}
title={todo.title}
description={todo.description}
/>
))}
</div>
</div>
);
}
}
export default App;

6. Crete ToDoListItem to delete ToDos

Adding an onClick event for the ToDoListItem component.

  • src/App.js
import React, { Component } from 'react';
import './App.css';
import ToDoListItem from "./ToDoListItem.js"

class App extends Component {

state = {
todoList: []
}

render() {
return (
<div className="App">
<form
className="App-form"
onSubmit={e => {
e.preventDefault();

const titleElement = e.target.elements["title"]
const descriptionElement = e.target.elements["description"];

this.setState(
{
todoList: this.state.todoList.concat({
title: titleElement.value,
description: descriptionElement.value
})
},
() => {
titleElement.value = "";
descriptionElement.value = "";
}
)
}}
>
<div>
<input
id="title"
placeholder="title"
/>
<textarea
id="description"
placeholder="description"
/>
</div>
<div>
<button
type="submit"
>
Register
</button>
</div>
</form>
<div>
{this.state.todoList.map(todo => (
<ToDoListItem
key={todo.title}
title={todo.title}
description={todo.description}
/>
))}
</div>
</div>
);
}
}

export default App;

The onClick attribute added to the ToDoListItem component of App.js is also passed to the props of the ToDoListItem component, as well as the title and description are.

At this point, the ToDoListItem component does not handle anything other than the title and description arguments, which is why onClick did not work.

We will deal with this by using JavaScript’s split assignment.

src/ToDoListItem.js

import React, { Component } from 'react';
import './ToDoListItem.css';
class ToDoListItem extends Component {
render() {
const {
title,
description,
...props
} = this.props;
return (
<div className="ToDoListItem" {...props}>
<div className="ToDoListItem-title">{title}</div>
<div className="ToDoListItem-description">{description}</div>
</div>
);
}
}
export default ToDoListItem;

7. Persistence of ToDoList

At this point, the ToDoList is initialized every time the page is refreshed.

So, you can use localStorage to make it persistent.

  • src/App.js
import React, { Component } from 'react';
import './App.css';
import ToDoListItem from "./ToDoListItem.js"
class App extends Component { state = {
todoList: JSON.parse(localStorage.getItem("todoList")) || []
}
addTodo = (item, callBack) => {
this.setState(
{
todoList: this.state.todoList.concat(item)
},
() => {
localStorage.setItem("todoList", JSON.stringify(this.state.todoList))
callBack && callBack()
}
)
}
removeTodo = (item, callBack) => {
this.setState(
{
todoList: this.state.todoList.filter(x => x !== item)
},
() => {
localStorage.setItem("todoList", JSON.stringify(this.state.todoList))
callBack && callBack()
}
)
}
render() {
return (
<div className="App">
<form
className="App-form"
onSubmit={e => {
e.preventDefault();
const titleElement = e.target.elements["title"]
const descriptionElement = e.target.elements["description"];
this.addTodo(
{
title: titleElement.value,
description: descriptionElement.value
},
() => {
titleElement.value = "";
descriptionElement.value = "";
}
)
}}
>
<div>
<input
id="title"
placeholder="title"
/>
<textarea
id="description"
placeholder="description"
/>
</div>
<div>
<button
type="submit"
>
Register
</button>
</div>
</form>
<div>
{this.state.todoList.map(todo => (
<ToDoListItem
key={todo.title}
title={todo.title}
description={todo.description}
onClick={() => this.removeTodo(todo)}
/>
))}
</div>
</div>
);
}
}
export default App;

How this works (Gif)

You can see todos still are after the page is refreshed in following the link

https://i.gyazo.com/d852058be42a9b82864562c26ee45e3a.mp4

Reference Document

https://note.com/shushuitie/n/ne432edf0fac1

--

--