Chào mừng trở lại với chuỗi bài viết “React Roadmap”! Chúng ta đã cùng nhau đi qua những khái niệm nền tảng của React như React là gì, Hiểu về JSX, Functional Components, và cách quản lý dữ liệu cơ bản với Props và State. Chúng ta cũng đã khám phá sức mạnh của useState và useEffect, cũng như cách tạo Custom Hooks để tái sử dụng logic. Hôm nay, chúng ta sẽ nâng cấp khả năng quản lý state của mình với một Hook mạnh mẽ khác: useReducer
.
useState
rất tuyệt vời cho việc quản lý các state đơn giản như boolean, string, number, hoặc các object/array nhỏ. Tuy nhiên, khi state của component trở nên phức tạp hơn—khi có nhiều giá trị state liên quan chặt chẽ với nhau, hoặc khi logic cập nhật state trở nên phức tạp và phụ thuộc vào state trước đó—thì việc sử dụng nhiều useState
có thể dẫn đến mã khó đọc, khó bảo trì và dễ gây lỗi. Đây chính là lúc useReducer
tỏa sáng.
Mục lục
useReducer Là Gì?
useReducer
là một Hook trong React cung cấp một cách thay thế cho useState
để quản lý state. Nó được xây dựng dựa trên một pattern phổ biến trong lập trình gọi là “reducer” (giảm thiểu), mà bạn có thể đã nghe đến trong thư viện quản lý state như Redux. Về cơ bản, useReducer
giúp bạn tập trung logic cập nhật state vào một hàm duy nhất, gọi là reducer function.
Cơ chế hoạt động của useReducer
xoay quanh ba khái niệm chính:
- State: Biến state hiện tại của bạn (tương tự như state trong
useState
). - Action: Một object (thường có thuộc tính
type
) mô tả *điều gì đã xảy ra* (chứ không phải làm thế nào để thay đổi state). Ví dụ:{ type: 'INCREMENT' }
,{ type: 'ADD_TODO', payload: 'Mua sữa' }
. - Reducer Function: Một hàm thuần khiết (pure function) nhận state hiện tại và một action, sau đó trả về state mới. Hàm này chứa tất cả logic về cách state thay đổi dựa trên các action khác nhau. Signature của nó là
(state, action) => newState
. - Dispatch Function: Một hàm mà bạn nhận được từ
useReducer
. Bạn gọi hàm này và truyền vào một action object để “kích hoạt” quá trình cập nhật state. Khidispatch
được gọi, React sẽ chạy reducer function với state hiện tại và action bạn đã dispatch, sau đó cập nhật state component với giá trị mới được trả về từ reducer.
Hãy hình dung nó như một cỗ máy: Bạn gửi một yêu cầu (action) đến cỗ máy (reducer), cỗ máy xử lý yêu cầu đó dựa trên tình trạng hiện tại của nó (state hiện tại), và đưa ra một tình trạng mới (state mới).
Cú Pháp của useReducer
Cú pháp cơ bản của useReducer
trông như sau:
const [state, dispatch] = useReducer(reducer, initialState);
reducer
: Là hàm reducer mà bạn đã định nghĩa.initialState
: Là giá trị khởi tạo cho state của bạn.state
: Biến state hiện tại, giống như biến state từuseState
.dispatch
: Hàm dùng để gửi (dispatch) các action đến reducer.
useReducer
cũng có một tham số thứ ba tùy chọn cho phép bạn khởi tạo state một cách “lười biếng” (lazy initialization), rất hữu ích nếu giá trị khởi tạo state cần tính toán phức tạp hoặc dựa trên props:
const [state, dispatch] = useReducer(reducer, initialArg, init);
Trong đó, init
là một hàm. State khởi tạo sẽ được tính bằng cách gọi init(initialArg)
. Nếu tham số thứ ba được truyền vào, initialState
(tham số thứ hai) sẽ được sử dụng như là đối số đầu tiên cho hàm init
.
Ví Dụ Đơn Giản: Bộ Đếm (Counter)
Chúng ta hãy bắt đầu với một ví dụ kinh điển: Bộ đếm. Mặc dù bạn hoàn toàn có thể dùng useState
cho việc này, ví dụ này giúp minh họa cách hoạt động của useReducer
một cách rõ ràng.
Đầu tiên, định nghĩa state khởi tạo:
const initialState = { count: 0 };
Tiếp theo, định nghĩa hàm reducer. Hàm này nhận state hiện tại và action, sau đó quyết định cách cập nhật state:
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState; // Có thể reset về giá trị khởi tạo ban đầu
default:
// Luôn ném lỗi nếu gặp action không xác định
throw new Error();
}
}
Lưu ý rằng hàm reducer phải là hàm *thuần khiết*: nó không được thay đổi state trực tiếp (immutable update) mà phải trả về một object state *mới*. Nó cũng không được thực hiện các tác vụ phụ (side effects) như gọi API, DOM manipulation,…
Bây giờ, sử dụng useReducer
trong functional component:
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState;
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<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;
Trong ví dụ này:
- State được lưu trong object
{ count: 0 }
, truy cập thông qua biếnstate
. - Các nút nhấn không trực tiếp thay đổi state. Thay vào đó, chúng gọi hàm
dispatch
và truyền vào một object action mô tả ý định của người dùng (ví dụ:{ type: 'increment' }
). - Hàm
dispatch
gửi action này đến hàmreducer
. - Hàm
reducer
nhận action và state hiện tại, sau đó trả về state mới dựa trên loại action. - React nhận state mới từ reducer và re-render component
Counter
với giá trị state đã cập nhật.
Cách tiếp cận này có vẻ dài dòng hơn useState(0)
và setCount(count + 1)
cho một ví dụ đơn giản như thế này. Nhưng nó thể hiện rõ ràng sự tách biệt giữa “điều gì xảy ra” (action) và “làm thế nào để thay đổi state” (reducer logic), điều này trở nên cực kỳ quan trọng khi state và logic trở nên phức tạp hơn.
Tại Sao Nên Dùng useReducer? Lợi Ích Mang Lại
Mặc dù useState
là lựa chọn mặc định cho hầu hết các state đơn giản, useReducer
mang lại nhiều lợi ích đáng kể trong các trường hợp phức tạp:
- Tập Trung Logic Cập Nhật State: Thay vì có nhiều hàm setter state rải rác trong component (và có thể cả trong
useEffect
), tất cả logic cập nhật state được gom lại trong một hàm reducer duy nhất. Điều này giúp mã nguồn gọn gàng, dễ theo dõi và bảo trì hơn. - Quản Lý State Liên Quan: Khi state của bạn là một object hoặc array phức tạp với nhiều thuộc tính liên quan và các thuộc tính này thường thay đổi cùng nhau hoặc dựa trên sự thay đổi của nhau,
useReducer
giúp quản lý các mối quan hệ này một cách mạch lạc hơn. Thay vì gọi nhiều hàmsetX
,setY
,setZ
, bạn dispatch một action duy nhất, và reducer xử lý tất cả các cập nhật liên quan trong một lần. - State Cập Nhật Phụ Thuộc vào State Trước Đó: Reducer function luôn nhận state *mới nhất* làm đối số đầu tiên. Điều này đảm bảo rằng logic cập nhật state luôn dựa trên giá trị state hiện tại chính xác, tránh được các vấn đề tiềm ẩn khi sử dụng các hàm setter của
useState
với các giá trị state cũ (stale closures). - Khả Năng Dự Đoán và Kiểm Thử: Vì reducer là một hàm thuần khiết, với cùng một input (state hiện tại và action), nó luôn trả về cùng một output (state mới). Điều này làm cho logic cập nhật state trở nên dễ dự đoán và cực kỳ dễ kiểm thử một cách độc lập, không cần render component.
- Truyền Dispatch Xuống Dưới Dễ Dàng Hơn: Khi cần truyền khả năng thay đổi state xuống các component con sâu hơn (prop drilling), việc truyền một hàm
dispatch
duy nhất thường đơn giản hơn nhiều so với việc truyền xuống nhiều hàm setter từuseState
. Khi kết hợp với Context API (chủ đề của bài tiếp theo trong Roadmap), bạn có thể tạo ra một giải pháp quản lý state toàn cục mạnh mẽ và sạch sẽ.
useReducer vs. useState: Chọn Công Cụ Phù Hợp
Khi nào nên dùng useReducer
thay vì useState
? Dưới đây là bảng so sánh giúp bạn đưa ra quyết định:
Tính năng | useState |
useReducer |
---|---|---|
Độ phức tạp State | Đơn giản (boolean, number, string, object/array nhỏ và độc lập) | Phức tạp (object/array lớn, nhiều thuộc tính liên quan) |
Logic cập nhật State | Đơn giản, độc lập (ví dụ: toggle boolean, increment/decrement một số) | Phức tạp, phụ thuộc vào state trước đó, hoặc nhiều cập nhật liên quan xảy ra cùng lúc |
Số lượng State liên quan | Ít, các giá trị state không liên quan chặt chẽ | Nhiều, các giá trị state có mối quan hệ logic |
Khả năng dự đoán & Kiểm thử | Thường đơn giản, nhưng logic phức tạp có thể nằm rải rác | Rất cao, logic cập nhật state tập trung trong hàm thuần khiết (reducer) |
Mã boilerplate | Ít cho state đơn giản | Nhiều hơn cho state đơn giản, nhưng ít hơn cho state phức tạp khi so với nhiều useState |
Tái sử dụng logic cập nhật | Khó tái sử dụng trực tiếp | Có thể tái sử dụng logic reducer giữa các component |
Như bảng trên cho thấy, useReducer
không phải là sự thay thế cho useState
, mà là một công cụ bổ sung trong hộp công cụ của bạn. Hãy chọn useState
cho những gì nó làm tốt (state đơn giản, độc lập) và useReducer
khi bạn đối mặt với sự phức tạp.
Ví Dụ Thực Tế Hơn: Quản lý State Form
Hãy xem xét một ví dụ phức tạp hơn một chút: một form với nhiều trường nhập liệu và logic validation đơn giản. Quản lý state cho từng trường bằng useState
có thể nhanh chóng trở nên cồng kềnh.
State khởi tạo có thể là một object:
const initialFormState = {
name: '',
email: '',
password: '',
errors: {},
isSubmitting: false,
};
Các action có thể bao gồm thay đổi giá trị trường, báo lỗi validation, gửi form, reset form, v.v. Mỗi action sẽ cần một type
và có thể có thêm dữ liệu (payload) như tên trường và giá trị mới:
const formReducer = (state, action) => {
switch (action.type) {
case 'HANDLE_INPUT_CHANGE':
return {
...state, // Luôn giữ lại state cũ
[action.field]: action.value, // Cập nhật trường cụ thể
errors: { // Có thể xóa lỗi khi người dùng gõ lại
...state.errors,
[action.field]: undefined
}
};
case 'SET_ERRORS':
return {
...state,
errors: action.errors // Cập nhật object lỗi
};
case 'SET_SUBMITTING':
return {
...state,
isSubmitting: action.isSubmitting
};
case 'RESET_FORM':
return initialFormState; // Reset về trạng thái ban đầu
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
Và đây là cách sử dụng trong component form:
import React, { useReducer } from 'react';
const initialFormState = {
name: '',
email: '',
password: '',
errors: {},
isSubmitting: false,
};
const formReducer = (state, action) => {
switch (action.type) {
case 'HANDLE_INPUT_CHANGE':
return {
...state,
[action.field]: action.value,
errors: {
...state.errors,
[action.field]: undefined
}
};
case 'SET_ERRORS':
return {
...state,
errors: action.errors
};
case 'SET_SUBMITTING':
return {
...state,
isSubmitting: action.isSubmitting
};
case 'RESET_FORM':
return initialFormState;
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
function SimpleForm() {
const [formState, dispatch] = useReducer(formReducer, initialFormState);
const handleInputChange = (e) => {
const { name, value } = e.target;
dispatch({
type: 'HANDLE_INPUT_CHANGE',
field: name,
value: value,
});
};
const handleSubmit = (e) => {
e.preventDefault();
// Logic validation ở đây
const formErrors = {};
if (!formState.name) formErrors.name = 'Tên không được để trống';
if (!formState.email) formErrors.email = 'Email không được để trống';
// ... các validation khác ...
if (Object.keys(formErrors).length > 0) {
dispatch({ type: 'SET_ERRORS', errors: formErrors });
} else {
dispatch({ type: 'SET_SUBMITTING', isSubmitting: true });
console.log('Form data:', formState);
// Simulate API call
setTimeout(() => {
alert('Form submitted!');
dispatch({ type: 'SET_SUBMITTING', isSubmitting: false });
dispatch({ type: 'RESET_FORM' }); // Reset form sau khi submit
}, 1000);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Tên:</label>
<input
type="text"
name="name"
value={formState.name}
onChange={handleInputChange}
/>
{formState.errors.name && <p style={{ color: 'red' }}>{formState.errors.name}</p>}
</div>
<div>
<label>Email:</label>
<input
type="email"
name="email"
value={formState.email}
onChange={handleInputChange}
/>
{formState.errors.email && <p style={{ color: 'red' }}>{formState.errors.email}</p>}
</div>
<div>
<label>Mật khẩu:</label>
<input
type="password"
name="password"
value={formState.password}
onChange={handleInputChange}
/>
{formState.errors.password && <p style={{ color: 'red' }}>{formState.errors.password}</p>}
</div>
<button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? 'Đang gửi...' : 'Gửi Form'}
</button>
<button type="button" onClick={() => dispatch({ type: 'RESET_FORM' })}>
Reset
</button>
</form>
);
}
export default SimpleForm;
Trong ví dụ này, toàn bộ state của form (các giá trị trường, lỗi, trạng thái gửi) được quản lý trong một object duy nhất bởi useReducer
. Hàm formReducer
xử lý tất cả các loại thay đổi có thể xảy ra với form state. Việc này giúp logic quản lý form state trở nên tập trung, dễ hiểu và dễ mở rộng hơn nhiều so với việc sử dụng useState
riêng cho từng trường và trạng thái.
Khi Nào Không Nên Dùng useReducer?
Như đã đề cập, useReducer
không phải là giải pháp cho mọi vấn đề về state. Đối với các state rất đơn giản và không liên quan đến state khác (ví dụ: state chỉ để bật/tắt một modal, hiển thị/ẩn một phần tử), useState
là đủ và làm cho code dễ đọc hơn nhiều.
Sử dụng useReducer
cho những trường hợp quá đơn giản có thể làm tăng boilerplate code một cách không cần thiết (phải định nghĩa action type, reducer, dispatch). Hãy cân nhắc độ phức tạp của state và logic cập nhật trước khi quyết định.
Kết Luận
useReducer
là một Hook mạnh mẽ trong React, cung cấp một cách tiếp cận có cấu trúc và dễ dự đoán hơn để quản lý state, đặc biệt là state phức tạp hoặc state có logic cập nhật phụ thuộc vào nhau. Bằng cách tách biệt “điều gì xảy ra” (action) khỏi “cách thay đổi state” (reducer), bạn có thể viết mã quản lý state gọn gàng, dễ kiểm thử và bảo trì hơn.
Trong lộ trình học React của mình, việc nắm vững useReducer
là một bước quan trọng giúp bạn xử lý các tình huống quản lý state phức tạp hơn mà không cần ngay lập tức nhảy sang các thư viện quản lý state toàn cục lớn như Redux. Nó cũng là nền tảng để hiểu cách thức hoạt động của các thư viện đó.
Tiếp theo trong chuỗi bài viết React Roadmap, chúng ta sẽ khám phá Context API, một công cụ tuyệt vời khi kết hợp với useReducer
để chia sẻ state phức tạp giữa các component mà không cần prop drilling. Hãy cùng chờ đón nhé!
Các bài viết khác trong chuỗi React Roadmap:
- React Roadmap – Lộ trình học React 2025
- React Là Gì? Vì Sao Nó Chiếm Lĩnh Thế Giới Frontend
- Class vs Functional Components: Bước Chuyển Lớn Trong Thế Giới React Hiện Đại
- Hiểu về JSX: Khi JavaScript Gặp Gỡ Markup
- React Props vs State: Ai Kiểm Soát Dữ Liệu Gì? (React Roadmap)
- Làm Chủ Conditional Rendering trong React (React Roadmap)
- Kết hợp Component trong React: Tái sử dụng thật dễ dàng (React Roadmap)
- Vòng Đời Component trong React: Từ Khởi Tạo Đến Kết Thúc (React Roadmap)
- Làm việc với Danh sách và Key trong React (React Roadmap)
- Xử Lý Sự Kiện trong React: Cách Tiếp Cận ‘React Way’ (React Roadmap)
- Refs trong React là gì? Truy cập DOM Trực Tiếp Đúng Cách (React Roadmap)
- React Roadmap: Render Props vs Higher Order Components: Các Mẫu Thiết Kế Trong Thực Tế
- useState và useEffect: Siêu Năng Lực Nhập Môn của React (React Roadmap)
- Tạo Custom Hooks trong React: Biến Tái Sử Dụng Mã Thành Nghệ Thuật (React Roadmap)
- Khi nào và Vì sao nên dùng useCallback, useMemo, và useRef (React Roadmap)