Picture of the author

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.

8m read time

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 : 
...

2023 © Thang Tran