Khi xây dựng ứng dụng Redux, việc lựa chọn middleware xử lý logic bất đồng bộ phù hợp có thể ảnh hưởng lớn đến khả năng mở rộng, hiệu năng và khả năng bảo trì. Bạn nên chọn Redux-Thunk hay Redux-Saga? Câu trả lời phụ thuộc vào độ phức tạp của nghiệp vụ và yêu cầu kỹ thuật cụ thể.
Nguyên lý hoạt động
Redux-Thunk mở rộng hành vi của dispatch bằng cách cho phép action creator trả về một hàm thay vì đối tượng thuần. Hàm này nhận dispatch và getState làm tham số, từ đó hỗ trợ gọi API bất đồng bộ:
const thunkMiddleware = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
Trong khi đó, Redux-Saga sử dụng hàm sinh (generator functions) và các Effects khai báo để tách biệt hoàn toàn logic side-effect ra khỏi action creators. Các tác vụ như gọi API, lắng nghe hành động, hoặc quản lý luồng được viết trong các saga riêng biệt, giúp dễ kiểm thử và điều khiển.
Tổ chức mã nguồn
Với Thunk, logic bất đồng bộ thường nằm trực tiếp trong action creator:
export const loadUserProfile = (id) => async (dispatch) => {
dispatch({ type: 'LOAD_PROFILE_START' });
try {
const data = await api.fetchProfile(id);
dispatch({ type: 'LOAD_PROFILE_SUCCESS', payload: data });
} catch (err) {
dispatch({ type: 'LOAD_PROFILE_ERROR', payload: err.message });
}
};
Còn với Saga, logic này được di chuyển sang file riêng và kích hoạt dựa trên hành động được gửi đến store:
function* handleProfileLoad(action) {
try {
yield put({ type: 'LOAD_PROFILE_START' });
const profile = yield call(api.fetchProfile, action.payload.id);
yield put({ type: 'LOAD_PROFILE_SUCCESS', payload: profile });
} catch (error) {
yield put({ type: 'LOAD_PROFILE_ERROR', payload: error.message });
}
}
function* watchProfileRequests() {
yield takeLatest('LOAD_PROFILE_REQUEST', handleProfileLoad);
}
Quản lý đồng thời và hủy tác vụ
Khi người dùng nhấn nút nhiều lần liên tiếp, bạn cần tránh gửi nhiều yêu cầu trùng lặp. Trong Thunk, điều này đòi hỏi cơ chế theo dõi thủ công. Trong khi đó, Saga cung cấp sẵn các hiệu ứng như takeLatest để tự động hủy các tác vụ trước đó:
yield takeLatest('SEARCH_PRODUCTS', fetchSearchResults);
Ngoài ra, takeEvery cho phép xử lý mọi hành động (phù hợp với trường hợp cần thực thi song song), còn takeLeading chỉ khởi tạo tác vụ mới khi tác vụ trước đã hoàn tất — rất hữu ích cho cơ chế polling.
Các tình huống thực tế
Form đơn giản: Dùng Thunk nếu chỉ cần gửi dữ liệu và xử lý lỗi cơ bản.
export const submitFeedback = (data) => async (dispatch) => {
dispatch({ type: 'FEEDBACK_SUBMITTING' });
try {
await api.sendFeedback(data);
dispatch({ type: 'FEEDBACK_SUBMITTED' });
} catch (e) {
dispatch({ type: 'FEEDBACK_SUBMIT_FAILED', payload: e.message });
throw e;
}
};
Luồng nghiệp vụ phức tạp: Dùng Saga khi cần phối hợp nhiều bước như xác thực, kiểm kho, áp dụng mã giảm giá:
function* processOrder(action) {
const { items, coupon } = action.payload;
const [stockCheck, discount] = yield all([
call(checkStock, items),
call(applyCoupon, coupon)
]);
if (!stockCheck.available) {
yield put({ type: 'ORDER_REJECTED', reason: 'OUT_OF_STOCK' });
return;
}
const order = yield call(createOrder, { items, discount });
yield put({ type: 'ORDER_CONFIRMED', payload: order });
}
Xử lý lỗi và rò rỉ bộ nhớ
Saga hỗ trợ cơ chế hủy tác vụ (cancel) khi component unmount, giúp tránh cập nhật state sau khi component đã bị hủy:
function* profileSaga() {
const task = yield fork(fetchProfile);
yield take('PROFILE_SCREEN_UNMOUNT');
yield cancel(task);
}
Bên cạnh đó, bạn có thể cấu hình xử lý lỗi toàn cục khi khởi tạo middleware:
const sagaMiddleware = createSagaMiddleware({
onError: (error, { sagaStack }) => {
logger.report(error, { context: 'saga', stack: sagaStack });
}
});
Hướng dẫn lựa chọn
| Yếu tố | Redux-Thunk | Redux-Saga |
|---|---|---|
| Độ phức tạp logic | Thấp – gọi API đơn giản | Cao – nhiều bước, điều kiện, đồng thời |
| Quản lý tác vụ | Không hỗ trợ hủy/tái sử dụng | Hỗ trợ hủy, throttle, debounce, polling |
| Khả năng kiểm thử | Khó test logic bất đồng bộ | Dễ test nhờ Effects thuần |
| Quy mô nhóm | Nhỏ, prototype nhanh | Lớn, cần tách biệt rõ ràng |
Trong thực tế, bạn có thể kết hợp cả hai: dùng Thunk cho các module đơn giản và Saga cho những phần nghiệp vụ phức tạp. Việc chuyển đổi dần từ Thunk sang Saga cũng hoàn toàn khả thi nhờ kiến trúc tách biệt của Redux.