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ì đã. 😯

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.

middleware redux

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é :mrgreen:

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()

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ào App.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 Redux-Thunk vs 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 vs redux-saga

Biểu đồ 6 tháng gần đây

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
  • Thư viện nhẹ, dễ tích hợp
  • Code dễ đọc
  • Đơn giản, dễ học
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
Return được kết quả function middleware bên component
Nhược điểm Xử lý một số tác vụ đặc biệt sẽ tỏ ra khá yếu
  • Nặng hơn Thunk, tích hợp rắc rối hơn 1 một chút
  • Code khó đọc hơn
  • Phức tạp hơn, độ khó cao hơn
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. :mrgreen:

Cảm ơn các bạn đã đọc đến đây, see you next time! Nhớ like fanpage để nhận thông báo các bài viết tiếp theo nhé 😆

5.Tham khảo

Thunks in Redux: The Basics

Redux thật là đơn giản (phần cuối)