So sánh redux thunk vs redux saga

Có bao giờ bạn tự hỏi tại sao mình lại phải sử dụng một middleware như Redux Thunk, Redux Saga hay chưa? Hay bạn chỉ dùng vì thấy các tutorial trên mạng bảo nên dùng và thế là bạn dùng.

Dù gì thì cũng đến lúc bạn cần nhìn nhận lại rằng liệu chúng ta có thực sự cần một middleware hay không, ở bài viết này mình sẽ phân tích giữa cách viết thuần không middleware và cách dùng Redux Thunk nhé.


🥇Dispatch bất đồng bộ

Mình ví dụ người dùng đăng nhập vào trang web chúng ta, sau khi đăng nhập thành công thì hiển thị một cái toast thông báo, sau 5s thì cái thông báo đó tự động ẩn đi.

Đây là cách đơn giản nhất để làm trong Redux

js

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

store.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

Hoặc như thế này bên trong

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

7 component

js

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

Chỉ có một sự khác biệt là bên trong

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

7 component, thường thì bạn sẽ không truy cập trực tiếp đến store mà bạn sẽ nhận

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 thông qua prop (hoặc hook đối với React Hook). Tuy nhiên, không có sự khác biệt nào đáng kể.

Nếu bạn không muốn gõ lại khi

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 cùng các action từ các component khác nhau, bạn có thể tách action ra như thế này thay vì phải

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 một object

js

export function showNotification(text) {

return { type: 'SHOW_NOTIFICATION', text }

}

export function hideNotification() {

return { type: 'HIDE_NOTIFICATION' }

}

js

import { showNotification, hideNotification } from '../actions'

this.props.dispatch(showNotification('You just logged in.'))

setTimeout(() => {

this.props.dispatch(hideNotification())

}, 5000)

Hoặc nếu bạn đưa các action vào trong

export function showNotification(text) {

return { type: 'SHOW_NOTIFICATION', text }

}

export function hideNotification() {

return { type: 'HIDE_NOTIFICATION' }

}

2 thì bạn sẽ dùng như thế này

js

this.props.showNotification('You just logged in.')

setTimeout(() => {

this.props.hideNotification()

}, 5000)

Nãy giờ thì chúng ta chưa sử dụng bất cứ middleware nào hoặc concept nâng cao nào cả.


🥇Tách ra thành action bất đồng bộ

Cách tiếp cận bên trên làm việc tốt ở trong những trường hợp đơn giản, nhưng bạn có thể tìm thấy một vài vấn đề:

  • Nó làm cho bạn phải viết lại logic này ở bất kỳ đâu mà bạn muốn show thông báo.
  • Các thông báo không có ID để phân biệt với nhau, dễ dẫn đến hiện tượng dispatch export function showNotification(text) { return { type: 'SHOW_NOTIFICATION', text } } export function hideNotification() { return { type: 'HIDE_NOTIFICATION' } } 3 1 cái là tất cả các thông báo hiện có trên màn hình đều bị ẩn sớm hơn dự tính.

Để giải quyết những vấn đề này, chúng ta cần tách ra thành một function mà chỉ tập trung logic timeout và dispatch 2 action. Nó có thể trông như thế này:

js

function showNotification(id, text) {

return { type: 'SHOW_NOTIFICATION', id, text }

}

function hideNotification(id) {

return { type: 'HIDE_NOTIFICATION', id }

}

let nextNotificationId = 0

export function showNotificationWithTimeout(dispatch, text) {

const id = nextNotificationId++

dispatch(showNotification(id, text))

setTimeout(() => {

dispatch(hideNotification(id))
}, 5000)

}

Bây giờ thì các component có thể sử dụng

export function showNotification(text) {

return { type: 'SHOW_NOTIFICATION', text }

}

export function hideNotification() {

return { type: 'HIDE_NOTIFICATION' }

}

4 mà không bị duplicate đoạn logic trên hoặc gặp phải vấn đề ẩn hiện notification:

js

showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

js

showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')

Tại sao

export function showNotification(text) {

return { type: 'SHOW_NOTIFICATION', text }

}

export function hideNotification() {

return { type: 'HIDE_NOTIFICATION' }

}

5 có

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 như là đối số thứ nhất? Bởi vì nó cần

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 các action vào store. Bình thường một component thực hiện việc

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 nhưng vì chúng ta muốn một function ngoài làm việc này, chúng ta cần truyền

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 vào.

Nếu bạn có một singleton store được export từ một module nào đó, bạn có thể import nó và sử dụng

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 trực tiếp như thế này

js

export default createStore(reducer)

import store from './store'

let nextNotificationId = 0

export function showNotificationWithTimeout(text) {

const id = nextNotificationId++

store.dispatch(showNotification(id, text))

setTimeout(() => {

store.dispatch(hideNotification(id))
}, 5000)

}

showNotificationWithTimeout('You just logged in.')

showNotificationWithTimeout('You just logged out.')

Điều này trông có vẻ đơn giản hơn nhưng chúng ta không nên làm vậy. Nguyên nhân chính là bởi vì nó ép store phải là một singleton. Điều này làm cho nó khó tích hợp vào server rendering. Trên server, bạn sẽ muốn mỗi request có store riêng, để mỗi user khác nhau nhận một preload data khác nhau.

Một singleton cũng khó để test hơn.

Vì thế chúng ta không nên làm như thế, hoặc bạn chắc chắn trong tương lai đi nữa thì app cũng chỉ client-side thôi.

Quay trở lại với phiên bản trước đó:

js

let nextNotificationId = 0

export function showNotificationWithTimeout(dispatch, text) {

const id = nextNotificationId++

dispatch(showNotification(id, text))

setTimeout(() => {

dispatch(hideNotification(id))
}, 5000)

}

showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')

Cách này đã giải quyết vấn đề với việc lặp lại logic và ẩn hiện notification.


🥇Thunk Middleware

Với những app đơn giản, cách tiếp cận trên có vẻ ổn. Bạn không cần quan tâm đến middleware nếu bạn hài lòng với nó.

Trong những app lớn, có thể bạn sẽ gặp một vài bất tiện xung quanh nó.

Ví dụ, nó có vẻ không hay lắm khi chúng ta phải truyền

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 đi khắp nơi. Điều này làm cho việc phân tách container và conponent trở nên phức tạp hơn bởi vì bất cứ component nào mà

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 một Redux action bất đồng bộ thì phải nhận

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 như một prop. Bạn không thể bind action với

export function showNotification(text) {

return { type: 'SHOW_NOTIFICATION', text }

}

export function hideNotification() {

return { type: 'HIDE_NOTIFICATION' }

}

2 được nữa bởi vì

export function showNotification(text) {

return { type: 'SHOW_NOTIFICATION', text }

}

export function hideNotification() {

return { type: 'HIDE_NOTIFICATION' }

}

5 không thực sự là một action creator nữa rồi. Nó không return về một object (Redux action).

Thêm nữa, khá là không hay khi ta phải nhớ function nào là action đồng bộ như

import { showNotification, hideNotification } from '../actions'

this.props.dispatch(showNotification('You just logged in.'))

setTimeout(() => {

this.props.dispatch(hideNotification())

}, 5000)

6 và cái nào là bất đồng bộ như

export function showNotification(text) {

return { type: 'SHOW_NOTIFICATION', text }

}

export function hideNotification() {

return { type: 'HIDE_NOTIFICATION' }

}

5. Vì cách sử dụng chúng khác nhau nên bạn cũng phải cẩn thận nếu không sẽ dẫn đến những lỗi không đáng.

Chúng ta cần cách gì đó để cho Redux thấy được những action creator bất đồng bộ như là một trường hợp đặc biệt của action creator thay vì là một function khác biệt hoàn toàn.

Nếu bạn còn ở đây với mình thì bạn cũng nhận ra được vấn đề bên trong app của bạn, chào mừng bạn sử dụng Redux Thunk middleware.

Trong gist, Redux Thunk "dạy" cho Redux nhận biết được các action đặc biệt này.

js

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

0

Khi middleware này được enable, nếu bạn dispatch một function, Redux Thunk middleware sẽ đưa function đó một đối số dispatch. Redux Thunk cũng giúp cho reducer của bạn chỉ nhận plain object actions.

Redux Thunk cũng cho phép chúng ta khai báo

export function showNotification(text) {

return { type: 'SHOW_NOTIFICATION', text }

}

export function hideNotification() {

return { type: 'HIDE_NOTIFICATION' }

}

5 như một Redux action creator thông thường.

js

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

1

Để ý cách viết gần giống với phiên bản trước đó. Tuy nhiên nó không nhận vào

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 như đối số đầu tiên. Thay vào đó nó return một function mà nhận vào đối số là

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9.

Chúng ta sử dúng nó trong component như thế nào? Rõ ràng, chúng ta có thể viết như thế này:

js

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

2

Chúng ta đang dùng như một và truyền

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 vào.

Có vẻ nó còn trông "ngố" hơn phiên bản trước đó.

Nhưng như mình đã nói trước đó. Nếu Redux Thunk middleware được enable, bất cứ khi nào bạn

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 một function thay vì một object, middleware sẽ gọi function đó với

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 được truyền vào như đối số đầu tiên.

Vì thế chúng ta có thể làm như thế này

js

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

3

Cuối cùng,

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 một async action trông không khác với một sync action. Đây là điều tốt bởi vì component không cần quan tâm điều gì xảy ra bên trong action, mặc kệ nó là đồng bộ hay bất đồng bộ.

Nếu kết hợp với

export function showNotification(text) {

return { type: 'SHOW_NOTIFICATION', text }

}

export function hideNotification() {

return { type: 'HIDE_NOTIFICATION' }

}

2 thì cách chúng ta

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 sẽ ngắn gọn hơn nữa.

js

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

4


🥇Đọc state trong Thunk

Trong trường hợp bạn muốn get state hiện tại của Redux store, bạn có thể truyền

this.props.showNotification('You just logged in.')

setTimeout(() => {

this.props.hideNotification()

}, 5000)

7 như đối số thứ 2 vào function mà bạn return từ thunk action creator. Điều này cho phép thunk đọc state hiện tại của store.

js

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

5


🥇Return trong Thunk

Redux không quan tâm bạn return gì từ thunk, nhưng nó sẽ đưa cho bạn giá trị mà bạn return từ thunk sau khi

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 xong. Đó là lý do tại sao bạn có thể return một Promise từ thunk và đợi nó cho đến khi nó thành công bằng cách gọi

js

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

6


🥇Tóm lại

Đừng sử dụng bất cứ middleware nào từ Redux Thunk, Redux Saga nếu bạn thực sự không cần chúng và hiểu bạn đang làm gì.

Nếu app của bạn tương lai có thể mở rộng và bạn muốn nhận được những lợi ích mà thunk mang lại như giải quyết được vấn đề truyền

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })

setTimeout(() => {

this.props.dispatch({ type: 'HIDE_NOTIFICATION' })

}, 5000)

9 đi khắp mọi nơi trong component thì mình recommend là dùng ngay và luôn cho chắc. Dù gì nó cũng rất nhẹ.

Cảm ơn bạn đã đọc đến đây, hẹn gặp lại ở những bài viết tiếp theo.


🥇Tham khảo


Phew! Cuối cùng bạn cũng đã đọc xong. Bài viết này có hơi dài một tí vì mình muốn nó đầy đủ nhất có thể 😅

Chúng ta đều hiểu rằng Javascript và React không hề dễ, chúng có quá nhiều concept cần phải học. Mình cũng cảm thấy nó khó! Nay lại có thể Typescript nữa 🥲, thật sự khó nuốt.

Nhưng đừng lo: Bạn có thể nắm vững các kiến thức trên chỉ trong một khóa học

Mình đã bắt đầu code React vào năm 2019, và nó đã trở thành thư viện ưa thích của mình để xây dựng UI và web app. Mình cũng đã làm việc với nhiều framework khác như Angular, Vue nhưng thực sự chỉ có React là đem lại cho mình cảm xúc và sự hiệu quả. 💓

Nếu bạn đang gặp khó khăn với React, mình ở đây để giúp bạn!

Mình đã dành hơn 6 tháng để phát triển khóa học . Trong khóa này bạn sẽ được học mọi thứ về thư viện ReactJs, các kiến thức từ cơ bản cho đến nâng cao nhất, mục đích của mình là giúp bạn chinh phục

Nếu bạn cảm thấy bài viết này của mình hữu ích, mình nghĩ bạn sẽ thích hợp với phong cách dạy của mình. Không như bài viết này, khóa học là sự kết hợp giữa các bài viết, video, quizz, bài tập nhỏ và dự án lớn có thể xin việc được ngay. Học xong mình đảm bảo bạn sẽ lên tay ngay. 💪🏻