Xây dựng ứng dụng To Do List với React JS
ToDoList App là một ứng dụng quản lý danh sách các công việc cần được thưc hiện. Đây là ứng dụng đầu tiên mình làm khi mình học bất cứ 1 công nghệ nào mới như Flutter, React, Vue, Node,... Bài viết này mình sẽ xây dựng ứng dụng ToDoList bằng React JS.
ToDoList App là gì ?
ToDoList App là một ứng dụng quản lý danh sách các công việc cần được thưc hiện. Đây là ứng dụng đầu tiên mình làm khi mình học bất cứ 1 công nghệ nào mới như Flutter, React, Vue, Node,... Bài viết này mình sẽ xây dựng ứng dụng ToDoList bằng React JS.
Preview ToDoList App
Đoạn Code HTML :
<div class="app">
<div class="header">
<h2>To Do List - React</h2>
<div class="input">
<input type="text" placeholder="Title..." />
<span class="addBtn">Add</span>
</div>
</div>
<div class="body">
<ul>
<li>
<span class="checkedIcon"></span>
<span class="label"> Learn English </span>
<span class="close"> × </span>
</li>
<li class="checked">
<span class="checkedIcon"></span>
<span class="label"> Run </span>
<span class="close"> × </span>
</li>
</ul>
</div>
</div>
Đoạn Code CSS :
* {
box-sizing: border-box;
font-family: "Handlee", cursive;
}
html {
height: 100%;
}
body {
background-color: #f3f1f5;
}
.app {
height: 100vh;
width: 100%;
margin: auto;
padding: 1rem 0;
}
.header {
background-color: #22577a;
padding: 1rem 2rem;
color: white;
text-align: center;
font-size: 2rem;
height: 200px;
}
.header:after {
content: "";
display: table;
clear: both;
}
.header h2 {
margin: 1rem 0;
}
input {
margin: 0;
border: none !important;
border-radius: 0;
width: 75%;
padding: 10px;
float: left;
font-size: 16px;
}
input:focus {
outline: none;
}
.addBtn {
padding: 10px;
width: 25%;
background: #9d9d9d;
color: #fff;
font-weight: 600;
float: left;
text-align: center;
font-size: 16px;
cursor: pointer;
transition: 0.3s;
border-radius: 0;
}
.addBtn:hover {
background-color: #c8c6c6;
}
.body {
height: calc(100% - 220px);
overflow-y: auto;
/*padding-right: 0.5rem;*/
margin: 10px 0;
}
.body::-webkit-scrollbar {
width: 0.5rem;
}
.body::-webkit-scrollbar-thumb {
background: #11324d;
border-radius: 0.5rem;
}
.body::-webkit-scrollbar-thumb:hover {
background: #6b7aa1;
}
ul {
margin: 0;
padding: 0;
}
ul li {
margin: 0.25rem;
cursor: pointer;
position: relative;
padding: 12px 0;
background: #eee;
font-size: 18px;
transition: 0.2s;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
ul li:hover .close {
display: flex;
}
ul li:nth-child(odd) {
background: #f9f9f9;
}
ul li:hover {
background: #ddd;
}
ul li.checked .checkedIcon {
content: "";
position: absolute;
border-color: #fff;
border-style: solid;
border-width: 0 2px 2px 0;
top: 10px;
left: 16px;
transform: rotate(45deg);
height: 15px;
width: 7px;
}
ul li.checked {
background-color: #7f7c82;
color: #fff;
}
ul li.checked .label {
text-decoration: line-through;
}
.label {
position: absolute;
left: 40px;
right: 40px;
}
.close {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 40px;
font-weight: 700;
font-size: 1.2rem;
align-items: center;
justify-content: center;
display: none;
}
@media only screen and (max-width: 800px) {
.app {
width: calc(100% - 2rem);
}
.header h2 {
font-size: 1.5rem;
}
}
@media only screen and (max-width: 1200px) and (min-width: 800px) {
.app {
width: 700px;
}
}
@media only screen and (min-width: 1200px) {
.app {
width: 900px;
}
}
Giao diện trên điện thoại :
Giao diện trên desktop :
Cấu trúc project :
-
todolist-react
-
public
-
src
-
App.js
-
App.css
-
constants.js
-
...
-
-
package.json
-
...
-
Khai báo các constants :
export const TO_DO_DONE = 'TO_DO_DONE';
export const TO_DO_PROCESSING = 'TO_DO_PROCESSING';
export const LABEL_TO_DO_LIST_STORAGE = 'dataToDoList';
Khai báo các state :
Khởi tạo giá trí ban đầu cho state toDoList, trong trường hợp trước đó bạn đã sử dụng ứng dụng này trên trình duyệt của bạn, dữ liệu sẽ được lưu trên localStorage và cập nhật bất cứ khi nào bạn quay lại với ứng dụng toDoList.
const [toDoList, setToDoList] = useState(() => {
const dataToDoList = localStorage.getItem(LABEL_TO_DO_LIST_STORAGE);
const dataInitial = JSON.parse(dataToDoList);
return Array.isArray(dataInitial) ? dataInitial : [];
});
const [toDo, setToDo] = useState('');
Hàm cập nhật state toDo khi viết có event onChange của input :
const handleChange = (event) => {
setToDo(event.target.value)
}
Hàm xử lý việc thêm mới 1 toDo :
const addToDo = () => {
if (toDo !== '') {
setToDoList(prev => {
const now = new Date();
return [{
id: now.valueOf(),
label: toDo,
status: TO_DO_PROCESSING,
createdAt: now,
}].concat(prev)
})
setToDo('');
}
}
Thực hiện việc thêm mới 1 toDo khi ấn Enter :
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
addToDo();
}
}
Hàm xử lý việc xóa 1 toDoItem khỏi todoList :
const removeToDoItem = (id) => {
setToDoList(prev => {
return prev.filter((toDoItem, index) => {
return toDoItem.id !== id;
})
})
}
Hàm thay đổi trạng thái của toDo :
const onChangeToDoStatus = (id) => {
setToDoList(prev => {
return prev.map((toDoItem, index) => {
if (toDoItem.id === id) {
return {
...toDoItem,
status: toDoItem.status === TO_DO_DONE ? TO_DO_PROCESSING : TO_DO_DONE
}
} else {
return toDoItem;
}
})
})
}
Cập nhật dữ liệu localStorage khi state toDoList thay đổi :
useEffect(() => {
localStorage.setItem(LABEL_TO_DO_LIST_STORAGE, JSON.stringify(toDoList))
}, [toDoList])
Code JSX :
<div className="app">
<div className="header">
<h2>To Do List - React</h2>
<div className="input">
<input
onChange={handleChange}
type="text"
value={toDo}
placeholder="Title..."
onKeyDown={handleKeyDown}
/>
<span className="addBtn" onClick={() => addToDo()}>
Add
</span>
</div>
</div>
<div className="body">
<ul>
{toDoList.map((toDoItem, index) => {
return (
<li
key={index}
onClick={() => onChangeToDoStatus(toDoItem.id)}
className={
toDoItem.status === TO_DO_DONE ? "checked" : ""
}
>
<span className="checkedIcon"></span>
<span className="label">{toDoItem.label}</span>
<span
onClick={() => removeToDoItem(toDoItem.id)}
className="close"
>
×
</span>
</li>
);
})}
</ul>
</div>
</div>
Chi tiết các File trong Project
App.js :
import './App.css';
import {
useEffect,
useState
} from "react";
import {
LABEL_TO_DO_LIST_STORAGE,
TO_DO_DONE,
TO_DO_PROCESSING
} from "./constants";
const App = () => {
const [toDoList, setToDoList] = useState(() => {
const dataToDoList = localStorage.getItem(LABEL_TO_DO_LIST_STORAGE);
const dataInitial = JSON.parse(dataToDoList);
return Array.isArray(dataInitial) ? dataInitial : [];
});
const [toDo, setToDo] = useState('');
useEffect(() => {
localStorage.setItem(LABEL_TO_DO_LIST_STORAGE, JSON.stringify(toDoList))
}, [toDoList])
const handleChange = (event) => {
setToDo(event.target.value)
}
const addToDo = () => {
if (toDo !== '') {
setToDoList(prev => {
const now = new Date();
return [{
id: now.valueOf(),
label: toDo,
status: TO_DO_PROCESSING,
createdAt: now,
}].concat(prev)
})
setToDo('');
}
}
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
addToDo();
}
}
const removeToDoItem = (id) => {
setToDoList(prev => {
return prev.filter((toDoItem, index) => {
return toDoItem.id !== id;
})
})
}
const onChangeToDoStatus = (id) => {
setToDoList(prev => {
return prev.map((toDoItem, index) => {
if (toDoItem.id === id) {
return {
...toDoItem,
status: toDoItem.status === TO_DO_DONE ? TO_DO_PROCESSING : TO_DO_DONE
}
} else {
return toDoItem;
}
})
})
}
return ( <
div className = "app" >
<
div className = "header" >
<
h2 > To Do List - React < /h2> <
div className = "input" >
<
input onChange = {
handleChange
}
type = "text"
value = {
toDo
}
placeholder = "Title..."
onKeyDown = {
handleKeyDown
}
/> <
span className = "addBtn"
onClick = {
() => addToDo()
} > Add < /span> <
/div> <
/div> <
div className = "body" >
<
ul > {
toDoList.map((toDoItem, index) => {
return ( <
li key = {
index
}
onClick = {
() => onChangeToDoStatus(toDoItem.id)
}
className = {
toDoItem.status === TO_DO_DONE ? "checked" : ""
} >
<
span className = "checkedIcon" > < /span> <
span className = "label" > {
toDoItem.label
} <
/span> <
span onClick = {
() => removeToDoItem(toDoItem.id)
}
className = "close" > ×
<
/span> <
/li>
)
})
} <
/ul> <
/div> <
/div>
);
}
export default App;
App.css :
* {
box-sizing: border-box;
font-family: "Handlee", cursive;
}
html {
height: 100%;
}
body {
background-color: #f3f1f5;
}
.app {
height: 100vh;
width: 100%;
margin: auto;
padding: 1rem 0;
}
.header {
background-color: #22577a;
padding: 1rem 2rem;
color: white;
text-align: center;
font-size: 2rem;
height: 200px;
}
.header:after {
content: "";
display: table;
clear: both;
}
.header h2 {
margin: 1rem 0;
}
input {
margin: 0;
border: none !important;
border-radius: 0;
width: 75%;
padding: 10px;
float: left;
font-size: 16px;
}
input:focus {
outline: none;
}
.addBtn {
padding: 10px;
width: 25%;
background: #9d9d9d;
color: #fff;
font-weight: 600;
float: left;
text-align: center;
font-size: 16px;
cursor: pointer;
transition: 0.3s;
border-radius: 0;
}
.addBtn:hover {
background-color: #c8c6c6;
}
.body {
height: calc(100% - 220px);
overflow-y: auto;
/*padding-right: 0.5rem;*/
margin: 10px 0;
}
.body::-webkit-scrollbar {
width: 0.5rem;
}
.body::-webkit-scrollbar-thumb {
background: #11324d;
border-radius: 0.5rem;
}
.body::-webkit-scrollbar-thumb:hover {
background: #6b7aa1;
}
ul {
margin: 0;
padding: 0;
}
ul li {
margin: 0.25rem;
cursor: pointer;
position: relative;
padding: 12px 0;
background: #eee;
font-size: 18px;
transition: 0.2s;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
ul li:hover .close {
display: flex;
}
ul li:nth-child(odd) {
background: #f9f9f9;
}
ul li:hover {
background: #ddd;
}
ul li.checked .checkedIcon {
content: "";
position: absolute;
border-color: #fff;
border-style: solid;
border-width: 0 2px 2px 0;
top: 10px;
left: 16px;
transform: rotate(45deg);
height: 15px;
width: 7px;
}
ul li.checked {
background-color: #7f7c82;
color: #fff;
}
ul li.checked .label {
text-decoration: line-through;
}
.label {
position: absolute;
left: 40px;
right: 40px;
}
.close {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 40px;
font-weight: 700;
font-size: 1.2rem;
align-items: center;
justify-content: center;
display: none;
}
@media only screen and (max-width: 800px) {
.app {
width: calc(100% - 2rem);
}
.header h2 {
font-size: 1.5rem;
}
}
@media only screen and (max-width: 1200px) and (min-width: 800px) {
.app {
width: 700px;
}
}
@media only screen and (min-width: 1200px) {
.app {
width: 900px;
}
}
constants.js :
export const TO_DO_DONE = 'TO_DO_DONE';
export const TO_DO_PROCESSING = 'TO_DO_PROCESSING';
export const LABEL_TO_DO_LIST_STORAGE = 'dataToDoList';
Source Code :
https://github.com/trandinhthangdev/todolist-react
Demo :
...