0
0
Lập trình
Admin Team
Admin Teamtechmely

Sử dụng `useReducer` để quản lý trạng thái phức tạp trong React

Đăng vào 7 tháng trước

• 7 phút đọc

📋 Mục lục

  1. Giới thiệu về useReducer
  2. Khi nào nên sử dụng useReducer?
  3. Cách sử dụng useReducer
  4. Ví dụ thực tế: Danh sách công việc (Todo List)
  5. Thực hành tốt nhất với useReducer
  6. Các lỗi thường gặp và cách khắc phục
  7. Mẹo hiệu suất cho useReducer
  8. FAQ về useReducer

1. Giới thiệu về useReducer

useReducer là một hook trong React, cung cấp một cách để quản lý trạng thái phức tạp hơn so với useState. Nếu bạn gặp phải một useState đang xử lý một đối tượng lớn với nhiều hàm cập nhật khác nhau, useReducer có thể là giải pháp tối ưu. Với useReducer, bạn có thể tập trung toàn bộ logic cập nhật trạng thái trong một hàm gọi là "reducer", giúp luồng dữ liệu trở nên dễ dự đoán hơn và dễ dàng gỡ lỗi.

2. Khi nào nên sử dụng useReducer?

Bạn nên cân nhắc sử dụng useReducer khi:

  • Trạng thái tiếp theo phụ thuộc vào logic phức tạp dựa trên trạng thái trước đó.
  • Trạng thái có cấu trúc phức tạp (đối tượng lồng nhau, mảng đối tượng).
  • Logic cập nhật trạng thái cần được kiểm tra một cách riêng biệt.
  • Bạn muốn tối ưu hóa hiệu suất cho các thành phần có các cập nhật sâu, vì dispatch không thay đổi giữa các lần render.

3. Cách sử dụng useReducer

Cú pháp cơ bản để sử dụng useReducer như sau:

javascript Copy
const [state, dispatch] = useReducer(reducer, initialState);
  • reducer: Một hàm thuần túy (state, action) => newState, nhận trạng thái hiện tại và một "hành động", và trả về trạng thái mới.
  • initialState: Trạng thái khởi tạo, tương tự như useState.
  • state: Trạng thái hiện tại.
  • dispatch: Một hàm "gửi" hành động đến hàm reducer. Đây là cách duy nhất để kích hoạt một cập nhật trạng thái. Một hành động thường là một đối tượng với một thuộc tính type (ví dụ: { type: 'INCREMENT' }).

Luồng hoạt động: Thành phần → dispatch(action)reducer(state, action) → Trạng thái mới → Render lại.


4. Ví dụ thực tế: Danh sách công việc (Todo List)

Đây là ví dụ điển hình cho useReducer, vì nó liên quan đến nhiều loại cập nhật trên một mảng đối tượng.

1. Định nghĩa Reducer và Trạng thái Khởi tạo

javascript Copy
// components/TodoList.js

// Trạng thái khởi tạo: một mảng các công việc
const initialState = {
  todos: [
    { id: 1, text: 'Học `useReducer`', completed: true },
    { id: 2, text: 'Thực hành với một ví dụ', completed: false },
  ],
};

// Các hành động có thể gửi đi
export const ACTIONS = {
  ADD_TODO: 'add-todo',
  TOGGLE_TODO: 'toggle-todo',
  DELETE_TODO: 'delete-todo',
};

// Reducer chứa toàn bộ logic cập nhật
function reducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD_TODO:
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.payload.text, completed: false }],
      };
    case ACTIONS.TOGGLE_TODO:
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case ACTIONS.DELETE_TODO:
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload.id),
      };
    default:
      return state;
  }
}

2. Sử dụng useReducer trong Thành phần

javascript Copy
import React, { useReducer, useState } from 'react';

function TodoList() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const [newTodoText, setNewTodoText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (newTodoText.trim() === '') return;
    dispatch({ type: ACTIONS.ADD_TODO, payload: { text: newTodoText } });
    setNewTodoText('');
  };

  return (
    <div>
      <h3>Danh sách công việc với `useReducer`</h3>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
          placeholder="Thêm công việc mới"
        />
        <button type="submit">Thêm</button>
      </form>
      <ul>
        {state.todos.map(todo => (
          <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
            <button onClick={() => dispatch({ type: ACTIONS.TOGGLE_TODO, payload: { id: todo.id } })}>
              {todo.completed ? 'Khôi phục' : 'Hoàn thành'}
            </button>
            <button onClick={() => dispatch({ type: ACTIONS.DELETE_TODO, payload: { id: todo.id } })}>
              Xóa
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

5. Thực hành tốt nhất với useReducer

  1. Reducers Thuần túy: Một reducer không bao giờ nên sửa đổi trạng thái gốc (state) hoặc hành động (action). Nó luôn phải trả về một đối tượng trạng thái mới và không nên thực hiện các tác dụng phụ (như gọi API).
  2. Sử dụng payload cho dữ liệu: Thông thường, bạn nên truyền dữ liệu cần thiết cho việc cập nhật bên trong một thuộc tính payload trong đối tượng hành động. Ví dụ: { type: 'ADD', payload: { newItem } }.
  3. Xuất các hằng số hành động: Định nghĩa các type của hành động dưới dạng hằng số (như trong ví dụ ACTIONS) giúp tránh lỗi chính tả và dễ bảo trì hơn.
  4. Tách biệt reducer: Đối với các thành phần rất phức tạp hoặc để tái sử dụng logic, bạn có thể định nghĩa reducer và trạng thái khởi tạo trong một tệp riêng và nhập chúng vào.

6. Các lỗi thường gặp và cách khắc phục

  • Lỗi: Sửa đổi trạng thái trong reducer.

    javascript Copy
    // SAI ❌
    case ACTIONS.DELETE_TODO:
      const index = state.todos.findIndex(t => t.id === action.payload.id);
      state.todos.splice(index, 1);
      return state;
    • Giải pháp: Luôn trả về một bản sao mới của trạng thái.
    javascript Copy
    // ĐÚNG ✅
    case ACTIONS.DELETE_TODO:
      return { ...state, todos: state.todos.filter(t => t.id !== action.payload.id) };
  • Lỗi: Quên trường hợp default trong switch.

    • Giải pháp: Nếu reducer của bạn nhận một hành động không nhận diện, nó nên trả về trạng thái hiện tại mà không thay đổi. Câu lệnh default: return state; đảm nhận việc này. Nếu không có nó, reducer của bạn sẽ trả về undefined và ứng dụng sẽ bị lỗi.

7. Mẹo hiệu suất cho useReducer

  • Tránh gọi các hàm không cần thiết: Đảm bảo chỉ gọi dispatch khi thực sự cần thiết để tránh render lại không cần thiết.
  • Sử dụng React.memo: Nếu bạn có các thành phần con mà không cần cập nhật mỗi lần trạng thái thay đổi, hãy sử dụng React.memo để cải thiện hiệu suất.
  • Tối ưu hóa reducer: Đảm bảo logic trong reducer được tối ưu hóa để không thực hiện các phép toán nặng nề không cần thiết trong mỗi lần gọi.

8. FAQ về useReducer

Q1: useReducer có khác gì so với useState?
A: useReducer thường được sử dụng khi cần quản lý trạng thái phức tạp hơn, trong khi useState phù hợp hơn cho các trạng thái đơn giản.

Q2: Tôi có thể sử dụng useReducer với các thành phần như thế nào?
A: Bạn có thể sử dụng useReducer trong bất kỳ thành phần nào của React, giống như useState.

Q3: Có thể sử dụng useReducer trong các custom hooks không?
A: Có, bạn hoàn toàn có thể sử dụng useReducer trong các custom hooks để quản lý trạng thái phức tạp và tái sử dụng logic.


Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào