[React Basics] Quản lý State phức tạp với useReducer Hook trong React

Bạn đã bao giờ cảm thấy useState trở nên quá tải khi phải quản lý nhiều state liên quan đến nhau trong một component chưa? Khi logic cập nhật state ngày càng phức tạp và những dòng setSomething, setSomethingElse bắt đầu rối tung lên? Nếu câu trả lời là "Có", thì đã đến lúc bạn làm quen với một cái tên thầm lặng nhưng cực kỳ mạnh mẽ trong "kho vũ khí" của React: useReducer Hook.

Quản lý State phức tạp với useReducer Hook trong React

Bài viết này sẽ giúp bạn không chỉ hiểu useReducer là gì, mà còn biết chính xác khi nào nên dùng nó, cách dùng ra sao và làm thế nào để kết hợp nó tạo ra những giải pháp quản lý state thông minh và hiệu quả.

📖 useReducer là gì? Một cái nhìn trực quan

Về cơ bản, useReducer là một Hook được sử dụng để quản lý state, tương tự như useState, nhưng nó được tối ưu cho các state phức tạp và logic cập nhật cầu kỳ.

Nếu useState giống như một công tắc bật/tắt đơn giản, thì useReducer giống như một "trung tâm chỉ huy" (command center). Thay vì trực tiếp ra lệnh "hãy thay đổi state thành giá trị X", bạn sẽ gửi một "mệnh lệnh" (action) đến trung tâm chỉ huy. Trung tâm này (gọi là reducer) sẽ xem xét mệnh lệnh và state hiện tại, sau đó quyết định xem state mới sẽ trông như thế nào.

useReducer Diagram

Mô hình này bao gồm 4 thành phần chính:

  1. State: Dữ liệu mà component của bạn cần để hoạt động, giống hệt như trong useState.
  2. Action: Một object mô tả hành động bạn muốn thực hiện. Nó thường có một thuộc tính type (kiểu hành động, ví dụ: 'INCREMENT') và có thể có thêm payload (dữ liệu đi kèm).
  3. Reducer: Một hàm thuần túy (pure function) nhận vào state hiện tại và một action, sau đó trả về một state mới. Nó chính là bộ não xử lý logic. (state, action) => newState.
  4. Dispatch: Một hàm đặc biệt mà useReducer cung cấp cho bạn. Bạn gọi hàm dispatch và truyền vào một action để gửi "mệnh lệnh" đến reducer.

🤔 Khi nào nên chọn useReducer thay vì useState?

Đây là câu hỏi quan trọng nhất. useReducer không phải để thay thế useState. Mỗi cái có một thế mạnh riêng. Hãy rút useReducer ra khỏi "bao kiếm" khi bạn gặp các tình huống sau:

  • Logic state phức tạp: Khi việc cập nhật một state đòi hỏi nhiều bước tính toán hoặc phụ thuộc vào nhiều giá trị khác nhau. Ví dụ: quản lý giỏ hàng, form đăng ký nhiều bước.
  • State có nhiều giá trị phụ thuộc lẫn nhau: Khi bạn có một object state với nhiều thuộc tính, và việc cập nhật một thuộc tính này thường kéo theo sự thay đổi của các thuộc tính khác.
    • Thay vì dùng nhiều useState:
      const [isLoading, setIsLoading] = useState(false)
      const [data, setData] = useState(null)
      const [error, setError] = useState(null)
      
    • Bạn có thể gom chúng lại:
      const initialState = { isLoading: false, data: null, error: null }
      const [state, dispatch] = useReducer(reducer, initialState)
      
  • State tiếp theo phụ thuộc vào state trước đó: Mặc dù useState có thể xử lý việc này setCount(prevCount => prevCount + 1), nhưng khi logic phức tạp hơn, reducer sẽ giúp mã nguồn trở nên rõ ràng và dễ kiểm thử hơn rất nhiều.
  • Tối ưu hiệu năng (Performance Optimization): Khi bạn cần truyền logic cập nhật state xuống các component con sâu bên trong, việc truyền hàm dispatch thường tốt hơn là truyền các callback riêng lẻ. dispatch là một hàm ổn định và sẽ không bị tạo lại sau mỗi lần render.

🛠️ "Thực chiến" với useReducer: Từ cơ bản đến nâng cao

Hãy cùng đi qua các ví dụ thực tế để thấy sức mạnh của useReducer.

Ví dụ 1: Bộ đếm kinh điển (The Classic Counter)

Chúng ta sẽ bắt đầu với ví dụ đơn giản nhất để hiểu cú pháp và luồng hoạt động.

import React, { useReducer } from 'react'

// 1. Định nghĩa Initial State
const initialState = { count: 0 }

// 2. Viết hàm Reducer
// Nó nhận state hiện tại và action, trả về state mới.
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    case 'reset':
      return { count: 0 }
    default:
      throw new Error()
  }
}

function Counter() {
  // 3. Sử dụng useReducer trong Component
  // Nó trả về state hiện tại và hàm dispatch
  const [state, dispatch] = useReducer(reducer, initialState)

  return (
    <div>
      <h2>Count: {state.count}</h2>
      {/* 4. Gọi dispatch với một action để cập nhật state */}
      <button onClick={() => dispatch({ type: 'increment' })}>Tăng</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Giảm</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  )
}

export default Counter

Luồng hoạt động:

  1. Người dùng click nút "Tăng".
  2. Sự kiện onClick gọi hàm dispatch({ type: 'increment' }).
  3. React chuyển action { type: 'increment' }state hiện tại ({ count: 0 }) vào hàm reducer.
  4. Bên trong reducer, switch case khớp với 'increment' và trả về state mới là { count: 1 }.
  5. React nhận state mới, và re-render lại component Counter với state.count bây giờ là 1.

Ví dụ 2: Quản lý Form phức tạp

Hãy tưởng tượng một form đăng ký. Thay vì dùng nhiều useState cho name, email, password, chúng ta có thể gom lại.

import React, { useReducer } from 'react'

const initialState = {
  name: '',
  email: '',
  error: null,
}

function formReducer(state, action) {
  switch (action.type) {
    case 'SET_FIELD':
      // action.payload sẽ là { field: 'name', value: 'John' }
      return {
        ...state,
        [action.payload.field]: action.payload.value,
      }
    case 'SET_ERROR':
      return {
        ...state,
        error: action.payload,
      }
    case 'RESET':
      return initialState
    default:
      return state
  }
}

function RegistrationForm() {
  const [state, dispatch] = useReducer(formReducer, initialState)

  const handleChange = (e) => {
    dispatch({
      type: 'SET_FIELD',
      payload: { field: e.target.name, value: e.target.value },
    })
  }

  const handleSubmit = (e) => {
    e.preventDefault()
    if (!state.name || !state.email) {
      dispatch({
        type: 'SET_ERROR',
        payload: 'Vui lòng điền đầy đủ thông tin!',
      })
    } else {
      console.log('Form submitted:', state)
      dispatch({ type: 'RESET' })
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        value={state.name}
        onChange={handleChange}
        placeholder="Tên"
      />
      <input
        type="email"
        name="email"
        value={state.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <button type="submit">Đăng ký</button>
      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}
    </form>
  )
}

Ưu điểm ở đây là gì? Toàn bộ logic xử lý form (SET_FIELD, SET_ERROR, RESET) được gom gọn vào một nơi duy nhất là formReducer. Component RegistrationForm chỉ cần biết "gửi đi mệnh lệnh" mà không cần quan tâm chi tiết cách state được cập nhật. Điều này làm cho component sạch sẽ và dễ đọc hơn rất nhiều.

🤝 useReducer + useContext: Cặp đôi hoàn hảo để quản lý Global State

Khi ứng dụng của bạn lớn lên, bạn sẽ cần chia sẻ state giữa nhiều component khác nhau. Đây là lúc sức mạnh của useReducer thực sự tỏa sáng khi kết hợp với useContext.

Bằng cách này, bạn có thể tạo ra một "bộ chứa state" toàn cục mà không cần đến các thư viện phức tạp như Redux.

Mô hình hoạt động:

  1. Tạo một Context.
  2. Tạo một component "Provider" bao bọc toàn bộ ứng dụng (hoặc một phần của nó).
  3. Bên trong Provider, sử dụng useReducer để quản lý state.
  4. Cung cấp cả state và hàm dispatch xuống cho các component con thông qua Context.
  5. Bất kỳ component con nào cũng có thể "tiêu thụ" (consume) Context này để đọc state hoặc gọi dispatch để cập nhật state.

Đây chính là nền tảng của rất nhiều giải pháp quản lý state "nhẹ ký" trong hệ sinh thái React.

💡 useReducer vs. Redux: Khi nào dùng cái nào?

Nếu bạn đã biết đến Redux, bạn sẽ thấy useReducer có nhiều điểm tương đồng (reducer, action, dispatch). Vậy chúng khác nhau ở đâu?

Tiêu chíuseReducerRedux
Phạm viThường dùng cho state của component hoặc một nhóm component nhỏ.Được thiết kế cho global state của toàn ứng dụng.
Độ phức tạpRất đơn giản, là một phần của React. Không cần cài đặt thêm.Phức tạp hơn, cần cài đặt thư viện (redux, react-redux). Có nhiều khái niệm hơn (middleware, store, selectors).
MiddlewareKhông có sẵn.Hỗ trợ mạnh mẽ (Redux Thunk, Redux Saga) để xử lý các tác vụ bất đồng bộ (side effects).
BoilerplateRất ít.Nhiều hơn đáng kể (actions, action creators, store configuration).
Trường hợp sử dụngLý tưởng cho quản lý state phức tạp ở cấp component hoặc các ứng dụng vừa và nhỏ không muốn thêm thư viện ngoài.Phù hợp cho các ứng dụng lớn, có nhiều state toàn cục, logic bất đồng bộ phức tạp và cần các công cụ gỡ lỗi mạnh mẽ.

Lời khuyên: Hãy bắt đầu với useState. Khi state trở nên phức tạp, hãy nâng cấp lên useReducer. Nếu ứng dụng của bạn thực sự lớn và cần các tính năng nâng cao như middleware, lúc đó hãy cân nhắc đến Redux hoặc các giải pháp tương tự.

Kết luận: useReducer là một lựa chọn mạnh mẽ

useReducer không phải là một công cụ bạn sẽ dùng hàng ngày, nhưng nó là một "vũ khí" cực kỳ lợi hại trong những tình huống phù hợp. Nó giúp bạn tổ chức code một cách sạch sẽ, tách biệt logic ra khỏi giao diện, và làm cho việc quản lý các state phức tạp trở nên dễ thở hơn rất nhiều.

Bằng cách nắm vững useReducer, bạn đã tiến một bước dài trên con đường trở thành một nhà phát triển React chuyên nghiệp, có khả năng xây dựng những ứng dụng linh hoạt, dễ bảo trì và mở rộng. Đừng ngần ngại, hãy mở editor lên và thử sức ngay hôm nay!

Bài viết liên quan

[React Basics] Hướng dẫn thiết lập môi trường phát triển React

Hướng dẫn cách thiết lập môi trường phát triển React cho người mới bắt đầu. Tìm hiểu các bước cài đặt Node.js, npm, và tạo project React đầu tiên với Create React App hoặc Vite.

[React Basics] useState Hook: Cách quản lý State hiệu quả trong React

Hướng dẫn cách sử dụng useState Hook để quản lý state trong các function component của React. Tìm hiểu các ví dụ thực tế, từ cơ bản đến nâng cao, giúp bạn làm chủ React Hook quan trọng này.

[React Basics] Stateful và Stateless Components: Phân biệt và Ứng dụng trong React

Sự khác biệt cốt lõi giữa Stateful và Stateless Components trong React là gì? Tìm hiểu khi nào và tại sao nên sử dụng từng loại để tối ưu hiệu suất ứng dụng của bạn.

[React Basics] React là gì? Tại sao nên học React ngay hôm nay?

Tìm hiểu React là gì, tại sao nó là một trong những thư viện JavaScript phổ biến nhất thế giới. Nắm bắt được cách React xây dựng giao diện người dùng nhanh chóng và hiệu quả.