Tại sao chúng ta nên dùng một middleware như Redux Thunk
Châm ngôn của mình là học để kiếm tiền.
Vì thế mình build các khóa học của mình để giúp anh em tiến bộ nhanh hơn x10 lần , để kiếm được nhiều tiền hơn
- 🏆 React.js Super: Trở thành React.js Developer trong 7 ngày với mức thu nhập 20 triệu/tháng
- 🏆 Node.js Super: Giúp bạn học cách phân tích, thiết kế, deploy 1 API Backend bằng Node.js
- 🏆 Next.js Super: Mình sẽ chia sẻ từ A-Z kiến thức về Next.js, thứ giúp mình kiếm hơn 1 tỉ/năm
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
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 connected
component
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 connected
component, thường thì bạn sẽ không truy cập trực tiếp đến store mà bạn sẽ nhận dispatch
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 dispatch
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 dispatch
một object
// actions.js
export function showNotification(text) {
return { type: 'SHOW_NOTIFICATION', text }
}
export function hideNotification() {
return { type: 'HIDE_NOTIFICATION' }
}
// component.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 connect()
thì bạn sẽ dùng như thế này
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
HIDE_NOTIFICATION
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:
// actions.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) {
// Dựa vào ID để xác định ẩn hiện cái nào.
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
Bây giờ thì các component có thể sử dụng showNotificationWithTimeout
mà không bị duplicate đoạn logic trên hoặc gặp phải vấn đề ẩn hiện notification:
// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
Tại sao showNotificationWithTimeout()
có dispatch
như là đối số thứ nhất? Bởi vì nó cần dispatch
các action vào store. Bình thường một component thực hiện việc dispatch
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 dispatch
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 dispatch
trực tiếp như thế này
// store.js
export default createStore(reducer)
// actions.js
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)
}
// component.js
showNotificationWithTimeout('You just logged in.')
// otherComponent.js
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 đó:
// actions.js
// ...
let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.js
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 dispatch
đ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à dispatch
một Redux action bất đồng bộ thì phải nhận dispatch
như một prop. Bạn không thể bind action với connect()
được nữa bởi vì showNotificationWithTimeout()
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ư showNotification()
và cái nào là bất đồng bộ như showNotificationWithTimeout()
. 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.
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
const store = createStore(reducer, applyMiddleware(thunk))
// Nó vẫn nhận biết được plain object actions
store.dispatch({ type: 'INCREMENT' })
// Nhưng với thunk middleware, nó cũng nhận biết được các function
store.dispatch(function (dispatch) {
// ... có thể dispatch nhiều lần bên trong
dispatch({ type: 'INCREMENT' })
dispatch({ type: 'INCREMENT' })
dispatch({ type: 'INCREMENT' })
setTimeout(() => {
// ... ngay cả bất đồng bộ!
dispatch({ type: 'DECREMENT' })
}, 1000)
})
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 showNotificationWithTimeout()
như một Redux action creator thông thường.
// actions.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(text) {
return function (dispatch) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
Để ý 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 dispatch
như đối số đầu tiên. Thay vào đó nó return một function mà nhận vào đối số là dispatch
.
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:
// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)
Chúng ta đang dùng như một currying function và truyền dispatch
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 dispatch
một function thay vì một object, middleware sẽ gọi function đó với dispatch
được truyền vào như đối số đầu tiên.
Vì thế chúng ta có thể làm như thế này
// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))
Cuối cùng, dispatch
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 connect()
thì cách chúng ta dispatch
sẽ ngắn gọn hơn nữa.
// actions.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(text) {
return function (dispatch) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
// component.js
import { connect } from 'react-redux'
// ...
this.props.showNotificationWithTimeout('You just logged in.')
// ...
export default connect(mapStateToProps, { showNotificationWithTimeout })(MyComponent)
🥇Đọ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 getState
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.
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
return function (dispatch, getState) {
// Redux không không quan tâm bạn return gì trong thunk
if (!getState().areNotificationsEnabled) {
return
}
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
🥇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 dispatch
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
dispatch(someThunkReturningPromise()).then(...)
🥇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 dispatch
đ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
Từ một câu trả lời của Dan Abramov – founder Redux Thunk trên stackoverflow
Kiến thức trong khóa học Next.js này đã giúp mình kiếm hơn 1 tỉ đồng/năm
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ể 😅
Website bạn đang đọc được viết bằng Next.js TypeScript và tối ưu từng chi tiết nhỏ như SEO, hiệu suất, nội dung để đảm bảo bạn có trải nghiệm tốt nhất.
Với lượt view trung bình là 30k/tháng (dù website rất ít bài viết). Website này đem lại doanh thu 1 năm vừa qua là hơn 1 tỉ đồng
Đó chính là sức mạnh của SEO, sức mạnh của Next.js.
Mình luôn tin rằng kiến thức là chìa khóa giúp chúng ta đi nhanh nhất.
Mình đã dành hơn 6 tháng để phát triển khóa học Next.js Super | Dự án quản lý quán ăn & gọi món bằng QR Code. Trong khóa này các bạn sẽ được học mọi thứ về framework Next.js, 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 mức lương 25 - 30 triêu/tháng
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, 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. 💪🏻