Xây dựng ứng dụng Todo với React và hooks: useState, useEffect và lưu trữ cục bộ

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Sau khi làm Todo bằng JavaScript thuần, bạn sẽ thấy React giúp quản lý giao diện gọn gàng hơn thế nào: bạn mô tả giao diện theo dữ liệu, còn việc cập nhật DOM để React lo. Bài này hướng dẫn bạn dựng Todo bằng React với hooks, chia thành các component nhỏ.

Khởi tạo dự án với Vite

Cần Node.js đã cài. Vite là công cụ tạo dự án nhanh và nhẹ:

npm create vite@latest react-todo -- --template react
cd react-todo
npm install
npm run dev

Mở địa chỉ hiện trong terminal (thường là http://localhost:5173). Trong thư mục src, ta sẽ sửa App.jsx và tạo thêm vài component. Có thể xóa nội dung mẫu trong App.css và index.css nếu muốn giao diện sạch.

Tư duy của React

Ba ý chính. Một, giao diện là hàm của dữ liệu: cùng dữ liệu thì cùng giao diện. Hai, dữ liệu thay đổi theo thời gian được giữ trong state; khi state đổi, component được vẽ lại. Ba, dữ liệu chảy một chiều từ component cha xuống con qua props, còn con báo ngược lên cha bằng cách gọi hàm được truyền xuống.

Bước 1: Component App giữ state

import { useState, useEffect } from 'react';
import TodoForm from './TodoForm';
import TodoList from './TodoList';

function loadTodos() {
  try {
    return JSON.parse(localStorage.getItem('todos')) || [];
  } catch {
    return [];
  }
}

export default function App() {
  const [todos, setTodos] = useState(loadTodos);

  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  function addTodo(text) {
    setTodos((prev) => [...prev, { id: Date.now(), text, done: false }]);
  }

  function toggleTodo(id) {
    setTodos((prev) =>
      prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
    );
  }

  function removeTodo(id) {
    setTodos((prev) => prev.filter((t) => t.id !== id));
  }

  const remaining = todos.filter((t) => !t.done).length;

  return (
    <main style={{ maxWidth: 420, margin: '40px auto', fontFamily: 'sans-serif' }}>
      <h1>Việc cần làm</h1>
      <TodoForm onAdd={addTodo} />
      <TodoList todos={todos} onToggle={toggleTodo} onRemove={removeTodo} />
      <p>Còn {remaining} việc chưa xong</p>
    </main>
  );
}

Điều cần nhớ: đừng sửa trực tiếp mảng state (như dùng push), hãy tạo mảng mới bằng map, filter hoặc toán tử trải .... Hàm khởi tạo lười useState(loadTodos) chỉ đọc localStorage một lần khi tải trang. useEffect với mảng phụ thuộc [todos] chạy mỗi khi danh sách thay đổi để lưu lại.

Bước 2: Component form

import { useState } from 'react';

export default function TodoForm({ onAdd }) {
  const [text, setText] = useState('');

  function handleSubmit(e) {
    e.preventDefault();
    const value = text.trim();
    if (!value) return;
    onAdd(value);
    setText('');
  }

  return (
    <form onSubmit={handleSubmit} style={{ display: 'flex', gap: 8 }}>
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Nhập công việc..."
        style={{ flex: 1 }}
      />
      <button type="submit">Thêm</button>
    </form>
  );
}

Ô nhập này là controlled component: giá trị của nó do state điều khiển, mỗi lần gõ phím thì onChange cập nhật state.

Bước 3: Component danh sách

export default function TodoList({ todos, onToggle, onRemove }) {
  if (todos.length === 0) {
    return <p>Chưa có công việc nào.</p>;
  }

  return (
    <ul style={{ listStyle: 'none', padding: 0 }}>
      {todos.map((todo) => (
        <li key={todo.id} style={{ display: 'flex', gap: 8, padding: '6px 0' }}>
          <input
            type="checkbox"
            checked={todo.done}
            onChange={() => onToggle(todo.id)}
          />
          <span
            style={{
              flex: 1,
              textDecoration: todo.done ? 'line-through' : 'none',
            }}
          >
            {todo.text}
          </span>
          <button onClick={() => onRemove(todo.id)}>Xóa</button>
        </li>
      ))}
    </ul>
  );
}

Thuộc tính key giúp React nhận ra phần tử nào thay đổi, thêm hay xóa. Hãy dùng một định danh ổn định như id, tránh dùng chỉ số mảng khi danh sách có thể đổi thứ tự hoặc xóa phần tử. Ở đây ta dùng style nội tuyến cho gọn trong ví dụ; dự án thật nên dùng file CSS hoặc CSS Modules.

So sánh với bản JavaScript thuần

  • Không còn gọi document.createElement hay tự vẽ lại danh sách, bạn chỉ khai báo giao diện cho từng trạng thái.
  • Logic được chia thành các component nhỏ, dễ đọc và tái sử dụng.
  • Đổi lại, bạn cần học khái niệm mới: state, props, hooks và quy tắc không sửa trực tiếp dữ liệu.

Lỗi thường gặp

  • Quên key trong danh sách, console sẽ cảnh báo.
  • Gọi hook trong điều kiện hoặc vòng lặp, hooks phải luôn được gọi cùng thứ tự ở đầu component.
  • Sửa trực tiếp state nên giao diện không cập nhật.
  • Thiếu mảng phụ thuộc trong useEffect làm hiệu ứng chạy sau mỗi lần vẽ.

Kết luận

Bạn đã dùng useState, useEffect, props và danh sách có key, đủ để đọc hiểu phần lớn code React cơ bản. Hãy thử thêm bộ lọc, chế độ sửa công việc, hoặc đổi sang dùng useReducer để so sánh.

Để học React cùng các kỹ thuật hiện đại một cách có hệ thống, bạn có thể xem các khóa học tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment