Chọn lựa mọn middleware khi phát triển một dự án React cùng với Redux luôn là mối bận tâm của các anh chị em code front-end. Redux-Thunk và Redux-Saga là 2 cái tên phổ biến nhất trong các thư viện middleware Redux. Nếu bạn là người mới, chưa thử qua 2 thư viện này thì bài viết này chính xác là dành cho bạn. Nhưng trước tiên phải đi tìm hiểu middleware là gì đã. 😯
1. Middleware là gì?
Middleware được coi là một bước trung gian ở giữa, nhiệm vụ của nó là tạo ra các side-effect (99% là tương tác với API), xử lý trước khi gọi các action.
Liệu bạn có thực sự cần một lib middleware cho Redux hay không ?
Để trả lời câu hỏi này bạn xem ví dụ mình gọi API bên dưới
Đầu tiên ta có store/reducer.js
import * as types from './constant'; const initialState = { loading: false, error: null, user: null } export const rootReducer = (state = initialState, action) => { switch (action.type) { case types.GET_USER_REQUESTED: return { loading: true, user: null, error: null } case types.GET_USER_SUCCEED: return { loading: false, user: action.payload.user, error: null } case types.GET_USER_FAILED: return { loading: false, user: null, error: action.payload.error } default: return state } };
Tiếp theo là store/action.js
import * as types from './constant'; export const getUsersRequested = () => { return { type: types.GET_USER_REQUESTED } } export const getUsersSucceed = (user) => { return { type: types.GET_USER_SUCCEED, payload: { user } } } export const getUsersFailed = (error) => { return { type: types.GET_USER_FAILED, payload: { error } } }
Tạo store/store.js
import { createStore } from 'redux'; import { rootReducer } from './reducer'; export const store = createStore(rootReducer);
Cập nhật lại index.js
một chút nhé
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; import { Provider } from 'react-redux'; import { store } from './store/store'; ReactDOM.render( < Provider store = { store } > < React.StrictMode > < App / > < /React.StrictMode> </Provider > , document.getElementById('root')) serviceWorker.unregister()
Và service.js
( ở đây mình fake gọi API)
export const fetchUsers = () => { return new Promise((resolve) => { setTimeout(() => { resolve({ name: 'Xdevclass' }) }, 1000) }) }
Và cuối đến là App.js
component
import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { getUsersRequested, getUsersSucceed, getUsersFailed } from './store/action'; import { fetchUsers } from './service'; function App(props) { const { getUsersRequested, getUsersSucceed, getUsersFailed, user } = props useEffect(() => { getUsersRequested() fetchUsers().then((res) => { getUsersSucceed(res) }).catch((err) => getUsersFailed(err) }, [getUsersRequested, getUsersSucceed, getUsersFailed]) return <div className = 'App' > { user?.name } < /div> } const mapState = (state) => ({ user: state.user }) const mapDispatch = { getUsersRequested, getUsersSucceed, getUsersFailed } export default connect(mapState, mapDispatch)(App);
Như các bạn thấy đó, mình gọi API và tương tác với Redux mà không cần một lib middleware nào cả. Vấn đề ở đây là việc gọi API và dispatch các action nhàm chán cứ nhét vào trong lifecycle của component thì không clean cho lắm. Khi dự án lớn dần, nếu không kiểm soát tốt thì mỗi người sẽ làm mỗi kiểu, có người thay vì dispatch 1 loạt action trong component thì sẽ tạo một file middleware để làm. Bấy giờ tác giả Redux – Dan Abramov mới viết thêm Redux-Thunk để thống nhất cách tiếp cận middleware Redux cho các developer.
2. Redux-Thunk
Thunk là gì ?
Thunk là 1 high order function (HOF), nó là function mà return lại một function khác.
Ví dụ:
// Eager version function yell(text) { console.log(text + '!') } yell('bonjour') // 'bonjour!' // Lazy (or "thunked") version function thunkedYell(text) { return function thunk() { console.log(text + '!') } } const thunk = thunkedYell('bonjour') // no action yet. // wait for it… thunk() // 'bonjour!'
Áp dụng nguyên lý thunk thì tác giả Redux đã tạo ra Redux-Thunk chỉ với 14 dòng code . Wow, ngạc nhiên chưa. Mặc dầu đơn giản nhưng Redux-Thunk lại được dùng rất nhiều và xử lý được hầu như mọi trường hợp mà bạn gặp khi code.
Đây là cách viết lại cách gọi API đầu bài theo Redux-Thunk nhé
Đầu tiên tạo 1 file store/thunk.js
import { fetchUsers as _fetchUsers } from '../service'; import * as actions from './action'; export const fetchUsers = () => (dispatch) => { dispatch(actions.getUsersRequested()) return _fetchUsers().then((user) => dispatch(actions.getUsersSucceed(user))).catch((error) => dispatch(actions.getUsersFailed(error))) };
Sửa lại một chút store/store.js
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import { rootReducer } from './reducer'; export const store = createStore(rootReducer, applyMiddleware(thunk));
Tiếp theo là import thunk.js
vàoApp.js
component
import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { fetchUsers } from './store/thunk'; function App(props) { const { fetchUsers, user } = props useEffect(() => { fetchUsers().then((res) => { console.log(res) }) }, [fetchUsers]) return <div className = 'App' > { user?.name } < /div> } const mapState = (state) => ({ user: state.user }); const mapDispatch = { fetchUsers }; export default connect(mapState, mapDispatch)(App)
Luồn chạy của Thunk: dispatch 1 thunk function => thunk chạy => thunk dispatch các action liên quan => reducer bắt được các action liên quan => lưu state vào store
Các bạn thấy thế nào, code bây giờ gọn và dễ nhìn hơn đúng không. Dùng Redux-Thunk giúp ta tách biệt side-effect sang một bên nhưng vẫn giữ được giá trị return bên service.
Nhìn chung thì Redux-Thunk dễ code, dễ hiểu nhưng trong một số trường hợp đặc biệt thì Redux-Thunk tỏ ra chưa thực sự mạnh mẽ cho lắm. Điển hình như
- Tạm dừng 1 request hoặc hủy request khi đang gọi api
- Bài toán click vào button để fetch data, nếu click liên tục thì chỉ lấy những lần click sau cùng
- Tự động gọi lại request vài lần khi có sự cố mạng xảy ra
Còn một số yêu cầu khác phức tạp hơn nữa sau này làm project nhiều các bạn sẽ gặp. Để khắc phục vấn đề trên thì mình đề xuất 1 thư viện mạnh mẽ hơn đó là Redux-Saga.
3. Redux-Saga
Khác với Redux-Thunk thì Redux-Saga tạo ra phần side-effect độc lập với actions và mỗi action sẽ có một saga tương ứng.
Để nắm được Redux-Saga hoạt động như thế nào thì bạn phải hiểu được cách sử dụng Generator function của ES6.
Trở lại source code ban đầu nếu cấu hình theo Redux-Saga thì sẽ như sau.
Tạo file store/saga.js
import {
fetchUsers as _fetchUsers
} from '../service';
import * as actions from './action';
import * as types from './constant';
import {
call,
put,
takeEvery
} from 'redux-saga/effects';
function* fetchUsers() {
try {
const res = yield call(_fetchUsers) yield put(actions.getUsersSucceed(res))
} catch (error) {
yield put(actions.getUsersFailed(error))
}
};
export default function* userSaga() {
yield takeEvery(types.GET_USER_REQUESTED, fetchUsers)
};
Update lại file store/store.js
import { createStore, applyMiddleware } from 'redux'; import { rootReducer } from './reducer'; import createSagaMiddleware from 'redux-saga'; import saga from './saga'; const sagaMiddleware = createSagaMiddleware(); export const store = createStore(rootReducer, applyMiddleware(sagaMiddleware)); sagaMiddleware.run(saga);
Cuối cùng là dispatch một action trong App.js
component
import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { getUsersRequested } from './store/action'; function App(props) { const { getUsersRequested, user } = props useEffect(() => { getUsersRequested() }, [getUsersRequested]) return <div className = 'App' > { user?.name } < /div> }; const mapState = (state) => ({ user: state.user }); const mapDispatch = { getUsersRequested }; export default connect(mapState, mapDispatch)(App);
Luồn chạy của Saga: dispatch 1 action => reducer bắt được action => saga bắt được action => saga dispatch các action liên quan => reducer bắt được các action liên quan => lưu state vào store
Nếu ở Redux-Thunk thì ta sẽ dispatch trực tiếp một thunk function trong component. Nhưng ở Redux-Saga thì ta dispatch một action thông thường, saga sẽ lắng nghe action đó và sẽ thực hiện một side-effect.
Redux-Saga có syntax hơi lạ nên khi mới tiếp cận bạn sẽ khá bối rối để tìm hiểu luồn chạy của code. Nếu dùng một thời gian thì bạn sẽ quen thôi. Một số hàm bên saga mà bạn hay dùng như:
- Call (Gọi tới api hoặc 1 Promise, có truyền tham số)
- Fork: rẽ nhánh sang 1 generator khác.
- Take: tạm dừng cho đến khi nhận được action
- Race: chạy nhiều effect đồng thời, sau đó hủy tất cả nếu một trong số đó kết thúc.
- Call: gọi function. Nếu nó return về một promise, tạm dừng saga cho đến khi promise được giải quyết.
- Put: dispatch một action. (giống như dispatch của redux-thunk)
- Select: chạy một selector function để lấy data từ state.
- takeLatest: có nghĩa là nếu chúng ta thực hiện một loạt các actions, nó sẽ chỉ thực thi và trả lại kết quả của của actions cuối cùng.
- takeEvery: thực thi và trả lại kết quả của mọi actions được gọi.
4. So sánh giữa Redux-thunk và Redux-saga
Điểm chính của bài viết đây 😀 , nếu các bạn đọc từ đầu bài đến giờ thì cũng đã rút ra cho mình điểm mạnh yếu của từng thư viện rồi. Mình cũng tóm gọn lại một số ý như sau.
Về các chỉ số lượt tải, số sao github của 2 thư viện
Redux-Thunk có lượt tải xuống luôn ở mức gấp 3 lần Redux-Saga, còn Redux-Saga thì vượt trội hơn Redux-Thunk về số sao trên github
Một bảng so sánh nhẹ:
Redux-Thunk | Redux-Saga | |
Ưu điểm |
Return được kết quả function middleware bên component |
Nhiều function tiện lợi cho việc xử lý các tác vụ đặc biệt, đòi hỏi quá trình bất đồng bộ phức tạp |
Nhược điểm | Xử lý một số tác vụ đặc biệt sẽ tỏ ra khá yếu |
Không return được kết quả middleware bên component |
Có thể nói điểm mạnh của thằng này là điểm yếu của thằng kia 😛 . Lựa chọn là ở bạn, riêng với bản thân mình thì mình thích Redux-Thunk hơn. Vì Thunk đơn giản, dễ viết và không phải tác vụ đặc biệt lúc nào bạn cũng gặp phải cũng không có cách xử lý với thunk đâu.
Cảm ơn các bạn đã đọc đến đây, see you next time!