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.
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.
Mô hình này bao gồm 4 thành phần chính:
- State: Dữ liệu mà component của bạn cần để hoạt động, giống hệt như trong
useState
. - 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êmpayload
(dữ liệu đi kèm). - Reducer: Một hàm thuần túy (pure function) nhận vào
state
hiện tại và mộtaction
, sau đó trả về mộtstate
mới. Nó chính là bộ não xử lý logic.(state, action) => newState
. - Dispatch: Một hàm đặc biệt mà
useReducer
cung cấp cho bạn. Bạn gọi hàmdispatch
và truyền vào mộtaction
để 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)
- Thay vì dùng nhiều
- State tiếp theo phụ thuộc vào state trước đó: Mặc dù
useState
có thể xử lý việc nàysetCount(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:
- Người dùng click nút "Tăng".
- Sự kiện
onClick
gọi hàmdispatch({ type: 'increment' })
. - React chuyển action
{ type: 'increment' }
vàstate
hiện tại ({ count: 0 }
) vào hàmreducer
. - Bên trong
reducer
,switch
case khớp với'increment'
và trả về state mới là{ count: 1 }
. - React nhận state mới, và re-render lại component
Counter
vớistate.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:
- Tạo một Context.
- Tạo một component "Provider" bao bọc toàn bộ ứng dụng (hoặc một phần của nó).
- Bên trong Provider, sử dụng
useReducer
để quản lý state. - Cung cấp cả
state
và hàmdispatch
xuống cho các component con thông qua Context. - Bất kỳ component con nào cũng có thể "tiêu thụ" (consume) Context này để đọc
state
hoặc gọidispatch
để 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í | useReducer | Redux |
---|---|---|
Phạm vi | Thườ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ạp | Rấ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). |
Middleware | Khô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). |
Boilerplate | Rất ít. | Nhiều hơn đáng kể (actions, action creators, store configuration). |
Trường hợp sử dụng | Lý 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!