Redux Saga là một thư viện middleware của Redux, giúp quản lý các tác vụ bất đồng (side effect) trong ứng dụng Redux một cách đơn giản và dễ dàng hơn. Bằng cách sử dụng tối đa tính năng Generators (function*
) của ES6, nó cho phép viết code bất đồng bộ nhưng trông giống như code đồng bộ.
Redux Saga không chỉ tồn tại trong thế giới JavaScript mà còn được coi là một design pattern. Saga pattern là một cách để quản lý các transaction (giao dịch) dài với nhiều side effect hoặc nguy cơ tiềm ẩn. Với mỗi transaction thành công, cần có counter-transaction để revert (đảo ngược) transaction đó về trạng thái ban đầu nếu gặp sự cố.
Sơ đồ minh họa khái niệm Saga Pattern trong quản lý giao dịch
Side effect là những tác vụ trong lập trình hàm cần tương tác với thế giới bên ngoài, ví dụ như gọi API để lấy dữ liệu, lưu dữ liệu vào localStorage, hoặc đọc cookie từ trình duyệt. Những tác vụ này thường là bất đồng bộ và có thể làm thay đổi trạng thái của ứng dụng. Reducer trong Redux được yêu cầu là synchronous (đồng bộ) và pure (thuần khiết), tức là chỉ xử lý logic dựa trên input và trả về output mà không gây ra side effect. Do đó, Redux Saga được sử dụng để xử lý các side effect bên ngoài Reducer, giúp giữ cho Reducer luôn đơn giản và dễ kiểm tra.
Generator function là một loại hàm đặc biệt trong JavaScript, có khả năng tạm dừng thực thi và trả về giá trị nhiều lần. Từ khóa yield
được sử dụng để tạm dừng thực thi và trả về giá trị. Khác với hàm thông thường thực thi và trả về kết quả một lần, Generator function có thể thực thi, tạm dừng trả về kết quả và tiếp tục thực thi từ vị trí đã dừng.
Redux Saga hoạt động bằng cách lắng nghe các action được dispatch. Khi một action được dispatch, Redux Saga sẽ kiểm tra xem action đó có phải là action mà nó quan tâm hay không. Nếu đúng, nó sẽ thực thi một generator function tương ứng. Generator function này sẽ yield
ra các effect object. Effect object là các object chứa thông tin hướng dẫn middleware của Redux thực thi các hoạt động khác, ví dụ như gọi một hàm bất đồng bộ hoặc put một action mới tới store.
Vậy tại sao nên sử dụng Redux Saga? So với Redux Thunk, một middleware khác thường được sử dụng để xử lý side effect, Redux Saga mang lại nhiều lợi ích. Redux Thunk thường dẫn đến “callback hell” khi xử lý nhiều tác vụ bất đồng bộ lồng nhau, làm cho code khó đọc và khó bảo trì. Redux Saga, với việc sử dụng generator function và effect object, giúp code dễ đọc, dễ hiểu và dễ kiểm tra hơn. Redux Saga cho phép test các asynchronous flows dễ dàng và giữ cho action luôn pure.
Ví dụ về so sánh giữa Redux Thunk và Redux Saga trong việc xử lý một API request:
Redux Thunk:
import { API_BUTTON_CLICK, API_BUTTON_CLICK_SUCCESS, API_BUTTON_CLICK_ERROR } from './actions/consts';
import { getDataFromAPI } from './api';
const getDataStarted = () => ({ type: API_BUTTON_CLICK });
const getDataSuccess = data => ({ type: API_BUTTON_CLICK_SUCCESS, payload: data });
const getDataError = message => ({ type: API_BUTTON_CLICK_ERROR, payload: message });
const getDataFromAPI = () => {
return dispatch => {
dispatch(getDataStarted());
getDataFromAPI()
.then(data => {
dispatch(getUserSuccess(data));
})
.catch(err => {
dispatch(getDataError(err.message));
});
};
};
Redux Saga:
import { call, put, takeEvery } from 'redux-saga/effects';
import { API_BUTTON_CLICK, API_BUTTON_CLICK_SUCCESS, API_BUTTON_CLICK_ERROR } from './actions/consts';
import { getDataFromAPI } from './api';
export function* apiSideEffect(action) {
try {
const data = yield call(getDataFromAPI);
yield put({ type: API_BUTTON_CLICK_SUCCESS, payload: data });
} catch (e) {
yield put({ type: API_BUTTON_CLICK_ERROR, payload: e.message });
}
}
export function* apiSaga() {
yield takeEvery(API_BUTTON_CLICK, apiSideEffect);
}
Redux Saga giúp tách biệt logic xử lý side effect khỏi component và reducer, giúp code dễ dàng bảo trì, mở rộng và kiểm thử. Việc sử dụng try/catch
trong Redux Saga giúp việc xử lý lỗi dễ dàng hơn so với việc sử dụng promise chain trong Redux Thunk.