Mục lục
Xem mục lục
Redux giải quyết vấn đề gì
Redux quản lý state dùng chung giữa nhiều component không liên quan trực tiếp trong cây component, tránh phải truyền state qua nhiều tầng props (props drilling) chỉ để tới được component cần dùng.
Context API cũng giải quyết được vấn đề tương tự ở mức cơ bản, nhưng Redux phù hợp hơn khi:
- Có nhiều state độc lập, cập nhật thường xuyên, và nhiều component ở nhiều nhánh khác nhau cùng cần đọc/ghi.
- Cần middleware xử lý logic phức tạp xen giữa lúc dispatch action và lúc state thực sự đổi (logging, async, undo/redo).
- Cần công cụ debug mạnh: Redux DevTools cho phép xem lại toàn bộ lịch sử action, tua lại state ở bất kỳ thời điểm nào.
Ba nguyên tắc cốt lõi
Redux xây dựng trên 3 nguyên tắc:
- Single source of truth: toàn bộ state của ứng dụng nằm trong một object duy nhất, gọi là store.
- State là read-only: không sửa trực tiếp state, cách duy nhất để thay đổi là dispatch một action mô tả điều gì đã xảy ra.
- Thay đổi bằng pure function: reducer nhận state hiện tại và action, trả về state mới, không gây side effect, không sửa trực tiếp state cũ.
Store, Action, Reducer là gì
- Store: object duy nhất giữ toàn bộ state của ứng dụng, tạo ra từ reducer. Mọi thao tác đọc state đều đọc qua store, mọi thao tác đổi state đều phải đi qua store bằng cách dispatch action.
- Action: một object thuần mô tả điều gì đã xảy ra, luôn có field
type(thường là chuỗi), có thể kèm thêm dữ liệu cần thiết (payload). Action không tự thực hiện thay đổi gì cả, chỉ là mô tả sự kiện. - Reducer: một pure function nhận state hiện tại và một action, trả về state mới. Reducer không được gây side effect (gọi API, đọc/ghi biến ngoài, random…), và không được sửa trực tiếp state cũ, chỉ được trả về một state mới.
Ví dụ tối thiểu bằng Redux thuần (không dùng Toolkit) để thấy rõ cơ chế gốc:
import { createStore } from "redux";
// Reducer: pure function, nhận state + action, trả về state mới
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
}
// Store: nơi giữ state, tạo ra từ reducer
const store = createStore(counterReducer);
// Action: object mô tả điều gì đã xảy ra, luôn có field `type`
store.dispatch({ type: "INCREMENT" });
console.log(store.getState()); // { count: 1 }
dispatch gửi action tới store, store gọi reducer với state hiện tại và action đó, nhận về state mới, rồi thông báo cho mọi nơi đang lắng nghe (subscribe) biết state đã đổi.
Vấn đề của Redux thuần: boilerplate
Một thao tác đơn giản như “tăng số đếm” trong Redux thuần thường cần tới 3-4 phần riêng biệt: action type constant, action creator, nhánh switch-case trong reducer, và cập nhật state theo kiểu immutable thủ công:
// action-types.js
export const INCREMENT = "counter/increment";
// actions.js
export function increment() {
return { type: INCREMENT };
}
// reducer.js
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case INCREMENT:
return { ...state, count: state.count + 1 }; // phải tự spread, tự viết immutable
default:
return state;
}
}
Với state lồng nhiều cấp (object trong object, mảng trong object), việc tự viết immutable update (...state, ...state.nested) càng dài dòng và dễ sai sót, đặc biệt khi cần cập nhật một field sâu bên trong cấu trúc phức tạp.
Redux Toolkit: cách viết hiện đại
Redux Toolkit (RTK) là bộ công cụ chính thức, gộp action creator và reducer vào chung một chỗ qua createSlice, đồng thời dùng thư viện Immer bên dưới để cho phép viết code “trông như mutate” nhưng thực chất Immer tự tạo ra state mới immutable:
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { count: 0 },
reducers: {
increment: state => {
state.count += 1; // trông như mutate, nhưng Immer xử lý thành immutable update
},
decrement: state => {
state.count -= 1;
},
incrementBy: (state, action) => {
state.count += action.payload;
},
},
});
export const { increment, decrement, incrementBy } = counterSlice.actions;
export default counterSlice.reducer;
createSlice tự sinh action creator (increment(), decrement(), incrementBy(5)) và action type ("counter/increment") từ tên field trong reducers, không cần viết tay như Redux thuần. So với ví dụ trước, cùng một logic nhưng ngắn hơn đáng kể, không cần action type constant riêng, không cần tự viết ...state.
dispatch và kết nối component
Thư viện react-redux cung cấp useSelector (đọc state) và useDispatch (gửi action):
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement } from "./counterSlice";
function Counter() {
const count = useSelector(state => state.counter.count);
const dispatch = useDispatch();
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(decrement())}>-1</button>
</div>
);
}
Luồng dữ liệu một chiều: component gọi dispatch(action) → store chạy reducer → state mới được lưu vào store → mọi component đang useSelector đọc đúng phần state vừa đổi tự động re-render với giá trị mới. Component không bao giờ sửa trực tiếp state, luôn phải đi qua action.
Async logic: createAsyncThunk
Reducer bắt buộc phải là pure function, không được gọi API hay gây side effect trực tiếp bên trong. createAsyncThunk tách phần gọi API ra riêng, tự động sinh ra 3 action tương ứng với 3 trạng thái của một lời gọi bất đồng bộ:
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
export const fetchUser = createAsyncThunk("user/fetch", async (userId) => {
const res = await api.getUser(userId);
return res.data;
});
const userSlice = createSlice({
name: "user",
initialState: { data: null, status: "idle" },
reducers: {},
extraReducers: builder => {
builder
.addCase(fetchUser.pending, state => {
state.status = "loading";
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.status = "succeeded";
state.data = action.payload;
})
.addCase(fetchUser.rejected, state => {
state.status = "failed";
});
},
});
Gọi dispatch(fetchUser(userId)) sẽ tự động dispatch lần lượt pending (khi bắt đầu gọi), rồi fulfilled (nếu thành công) hoặc rejected (nếu lỗi), không cần tự quản lý 3 trạng thái này bằng tay như viết useState rời rạc.
So sánh Redux với Context API và React Query
Ba công cụ giải quyết ba vấn đề khác nhau, không thay thế hoàn toàn cho nhau:
| Công cụ | Phù hợp cho | Không phù hợp cho |
|---|---|---|
| Context API | State đơn giản, ít thay đổi, phạm vi nhỏ (theme, ngôn ngữ, thông tin user đăng nhập) | State phức tạp, thay đổi thường xuyên (dễ gây re-render toàn bộ consumer, đã nói ở bài Tối ưu hiệu năng React) |
| Redux | Global client state phức tạp, nhiều action, cần middleware/devtools mạnh, logic nghiệp vụ phức tạp | Cache dữ liệu từ server (tự quản lý loading/error/refetch/stale time thủ công, tốn công hơn hẳn) |
| React Query | Server state: dữ liệu từ API, cần cache, refetch, đồng bộ nhiều nơi (đã nói ở bài Optimistic Updates) | Client state thuần túy không liên quan tới server (trạng thái UI, form đang mở/đóng) |
Nhầm lẫn phổ biến: dùng Redux để lưu cache dữ liệu server, tự viết lại toàn bộ cơ chế loading/error/refetch/cache invalidation mà React Query đã làm sẵn. Redux phù hợp nhất với state thuộc về client, không phải bản sao của dữ liệu server.
Xu hướng hiện nay
Từ khi React Query (và các thư viện tương tự như SWR) phổ biến, phần lớn lý do trước đây phải dùng Redux (đặc biệt là createAsyncThunk/redux-thunk chỉ để fetch data) không còn cần thiết nữa. createAsyncThunk sinh ra chủ yếu để chuẩn hóa việc gọi API và quản lý pending/fulfilled/rejected, nhưng React Query làm đúng việc đó tốt hơn, ít code hơn, có sẵn cache/refetch/stale time.
Xu hướng phổ biến ở các dự án mới:
- React Query/SWR lo toàn bộ server state: dữ liệu từ API, cache, đồng bộ, optimistic update.
- Zustand, Jotai, hoặc chỉ Context/
useStatelo phần client state nhẹ: trạng thái UI, form đang mở/đóng, theme, các state không có bản sao trên server. - Redux (với Redux Toolkit) chỉ còn giữ chỗ khi thực sự cần: state client phức tạp dùng chung ở nhiều nhánh xa nhau trong cây component, cần Redux DevTools để debug time-travel, hoặc dự án đã đầu tư lớn vào Redux từ trước nên chi phí migrate không đáng.
- Redux Saga cũng theo xu hướng tương tự: phần fetch data nhường lại cho React Query, chỉ còn giữ vai trò ở chỗ điều phối side effect phức tạp không thuần là gọi API (WebSocket, luồng nghiệp vụ nhiều bước, cancellation phức tạp).
Nói ngắn gọn: Redux không “chết”, nhưng phạm vi sử dụng đã thu hẹp lại đáng kể, từ chỗ gần như mặc định cho mọi ứng dụng React cỡ vừa trở lên, xuống thành lựa chọn cho một nhóm bài toán cụ thể hơn nhiều.
Tổng kết
- Redux quản lý state dùng chung qua một store duy nhất, thay đổi chỉ qua dispatch action, xử lý bằng reducer thuần túy.
- Redux thuần đòi hỏi nhiều boilerplate (action type, action creator, immutable update thủ công).
- Redux Toolkit (
createSlice) gộp action + reducer, dùng Immer để viết code ngắn gọn nhưng vẫn immutable. useSelector/useDispatchkết nối component với store, luồng dữ liệu một chiều.createAsyncThunkxử lý side effect bất đồng bộ, tự sinh 3 trạng thái pending/fulfilled/rejected.- Redux hợp cho global client state phức tạp; Context hợp cho state đơn giản; React Query hợp cho server state, tránh dùng nhầm Redux để cache dữ liệu server.