Đừng rơi vào cái bẫy nghĩ rằng một thư viện nên quy định cách làm mọi thứ . Nếu bạn muốn làm một cái gì đó với thời gian chờ trong JavaScript, bạn cần sử dụng setTimeout
. Không có lý do tại sao các hành động Redux nên khác nhau.
Redux không cung cấp một số cách khác nhau để đối phó với những thứ không đồng bộ, nhưng bạn chỉ nên sử dụng những khi bạn nhận ra bạn đang lặp đi lặp lại quá nhiều mã. Trừ khi bạn gặp vấn đề này, hãy sử dụng những gì ngôn ngữ cung cấp và đưa ra giải pháp đơn giản nhất.
Viết nội tuyến mã Async
Đây là cách đơn giản nhất. Và không có gì cụ thể cho Redux ở đây.
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Tương tự, từ bên trong một thành phần được kết nối:
this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Sự khác biệt duy nhất là trong một thành phần được kết nối, bạn thường không có quyền truy cập vào cửa hàng, nhưng có được dispatch()
hoặc người tạo hành động cụ thể được tiêm làm đạo cụ. Tuy nhiên điều này không làm cho bất kỳ sự khác biệt cho chúng tôi.
Nếu bạn không thích mắc lỗi chính tả khi gửi cùng một hành động từ các thành phần khác nhau, bạn có thể muốn trích xuất các trình tạo hành động thay vì gửi các đối tượng hành động nội tuyến:
// 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 trước đây bạn đã ràng buộc họ với connect()
:
this.props.showNotification('You just logged in.')
setTimeout(() => {
this.props.hideNotification()
}, 5000)
Cho đến nay chúng tôi chưa sử dụng bất kỳ phần mềm trung gian hoặc khái niệm nâng cao nào khác.
Trích xuất trình tạo hành động Async
Cách tiếp cận trên hoạt động tốt trong các trường hợp đơn giản nhưng bạn có thể thấy rằng nó có một vài vấn đề:
- Nó buộc bạn phải sao chép logic này bất cứ nơi nào bạn muốn hiển thị thông báo.
- Các thông báo không có ID nên bạn sẽ có điều kiện cuộc đua nếu bạn hiển thị hai thông báo đủ nhanh. Khi hết thời gian chờ đầu tiên, nó sẽ gửi đi
HIDE_NOTIFICATION
, ẩn nhầm thông báo thứ hai sớm hơn sau khi hết thời gian.
Để giải quyết những vấn đề này, bạn sẽ cần trích xuất một hàm tập trung logic hết thời gian và gửi hai hành động đó. 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) {
// Assigning IDs to notifications lets reducer ignore HIDE_NOTIFICATION
// for the notification that is not currently visible.
// Alternatively, we could store the timeout ID and call
// clearTimeout(), but we’d still want to do it in a single place.
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
Bây giờ các thành phần có thể sử dụng showNotificationWithTimeout
mà không cần sao chép logic này hoặc có các điều kiện cuộc đua với các thông báo khác nhau:
// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
Tại sao không showNotificationWithTimeout()
chấp nhận dispatch
là đối số đầu tiên? Bởi vì nó cần phải gửi hành động đến cửa hàng. Thông thường một thành phần có quyền truy cập dispatch
nhưng vì chúng ta muốn một chức năng bên ngoài kiểm soát việc gửi đi, chúng ta cần cho nó quyền kiểm soát việc gửi đi.
Nếu bạn có một cửa hàng đơn lẻ được xuất từ một số mô-đun, bạn chỉ có thể nhập nó và dispatch
trực tiếp trên nó thay thế:
// 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 có vẻ đơn giản hơn nhưng chúng tôi không khuyến nghị phương pháp này . Lý do chính khiến chúng tôi không thích nó là vì nó buộc cửa hàng phải là một người độc thân . Điều này làm cho nó rất khó để thực hiện kết xuất máy chủ . Trên máy chủ, bạn sẽ muốn mỗi yêu cầu có cửa hàng riêng để những người dùng khác nhau nhận được dữ liệu tải sẵn khác nhau.
Một cửa hàng đơn lẻ cũng làm cho thử nghiệm khó hơn. Bạn không còn có thể chế nhạo một cửa hàng khi thử nghiệm người tạo hành động vì họ tham chiếu một cửa hàng thực cụ thể được xuất từ một mô-đun cụ thể. Bạn thậm chí không thể thiết lập lại trạng thái của nó từ bên ngoài.
Vì vậy, trong khi về mặt kỹ thuật bạn có thể xuất một cửa hàng đơn lẻ từ một mô-đun, chúng tôi không khuyến khích nó. Đừng làm điều này trừ khi bạn chắc chắn rằng ứng dụng của bạn sẽ không bao giờ thêm kết xuất máy chủ.
Quay trở lạ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.')
Điều này giải quyết các vấn đề với sự trùng lặp logic và cứu chúng ta khỏi các điều kiện chủng tộc.
Middleware
Đối với các ứng dụng đơn giản, cách tiếp cận nên đủ. Đừng lo lắng về phần mềm trung gian nếu bạn hài lòng với nó.
Tuy nhiên, trong các ứng dụng lớn hơn, bạn có thể tìm thấy những bất tiện nhất định xung quanh nó.
Chẳng hạn, có vẻ đáng tiếc là chúng ta phải vượt qua dispatch
. Điều này làm cho việc phân tách các thành phần chứa và trình bày trở nên khó khăn hơn bởi vì bất kỳ thành phần nào gửi các hành động Redux không đồng bộ theo cách trên đều phải chấp nhận dispatch
như một chỗ dựa để nó có thể vượt qua nó. Bạn không thể liên kết người tạo hành động với connect()
nữa vì showNotificationWithTimeout()
thực sự không phải là người tạo hành động. Nó không trả về một hành động Redux.
Ngoài ra, có thể khó xử khi nhớ chức năng nào là người tạo hành động đồng bộ thích showNotification()
và người trợ giúp không đồng bộ thích showNotificationWithTimeout()
. Bạn phải sử dụng chúng khác nhau và cẩn thận để không nhầm lẫn chúng với nhau.
Đây là động lực để tìm cách hợp thức hóa các mô hình cung cấp dispatch
chức năng của người trợ giúp và giúp Redux cảm thấy các nhà sáng tạo hành động không đồng bộ như một trường hợp đặc biệt của những người tạo hành động bình thường thay vì các chức năng hoàn toàn khác nhau.
Nếu bạn vẫn ở với chúng tôi và bạn cũng nhận ra đó là một vấn đề trong ứng dụng của mình, bạn có thể sử dụng phần mềm trung gian Redux Thunk .
Trong một ý chính, Redux Thunk dạy Redux nhận ra các loại hành động đặc biệt có chức năng thực tế:
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
const store = createStore(
reducer,
applyMiddleware(thunk)
)
// It still recognizes plain object actions
store.dispatch({ type: 'INCREMENT' })
// But with thunk middleware, it also recognizes functions
store.dispatch(function (dispatch) {
// ... which themselves may dispatch many times
dispatch({ type: 'INCREMENT' })
dispatch({ type: 'INCREMENT' })
dispatch({ type: 'INCREMENT' })
setTimeout(() => {
// ... even asynchronously!
dispatch({ type: 'DECREMENT' })
}, 1000)
})
Khi phần mềm trung gian này được bật, nếu bạn gửi một hàm , phần mềm trung gian Redux Thunk sẽ cung cấp cho nó dispatch
làm đối số. Nó cũng sẽ nuốt những hành động như vậy vì vậy đừng lo lắng về việc bộ giảm của bạn nhận được các đối số chức năng kỳ lạ. Bộ giảm tốc của bạn sẽ chỉ nhận được các hành động đối tượng đơn giản, được phát ra trực tiếp hoặc được phát ra bởi các chức năng như chúng tôi vừa mô tả.
Điều này có vẻ không hữu ích lắm phải không? Không phải trong tình huống đặc biệt này. Tuy nhiên, nó cho phép chúng tôi tuyên bố showNotificationWithTimeout()
là người tạo hành động Redux 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)
}
}
Lưu ý cách hàm gần giống với hàm chúng ta đã viết trong phần trước. Tuy nhiên, nó không chấp nhận dispatch
như là đối số đầu tiên. Thay vào đó, nó trả về một hàm chấp nhận dispatch
làm đối số đầu tiên.
Làm thế nào chúng ta sẽ sử dụng nó trong thành phần của chúng tôi? Chắc chắn, chúng ta có thể viết điều này:
// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)
Chúng tôi đang gọi trình tạo hành động async để có được chức năng bên trong muốn dispatch
, và sau đó chúng tôi vượt qua dispatch
.
Tuy nhiên điều này thậm chí còn khó xử hơn phiên bản gốc! Tại sao chúng ta thậm chí đi theo cách đó?
Bởi vì những gì tôi nói với bạn trước đây. Nếu Redux Thunk middleware được bật, bất cứ khi nào bạn cố gắng gửi một hàm thay vì một đối tượng hành động, phần mềm trung gian sẽ gọi hàm đó với dispatch
chính phương thức đó là đối số đầu tiên .
Vì vậy, chúng ta có thể làm điều này thay vào đó:
// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))
Cuối cùng, việc gửi một hành động không đồng bộ (thực sự, một loạt các hành động) trông không khác gì gửi một hành động đồng bộ đến thành phần. Điều này là tốt bởi vì các thành phần không nên quan tâm liệu một cái gì đó xảy ra đồng bộ hay không đồng bộ. Chúng tôi chỉ trừu tượng hóa đi.
Lưu ý rằng vì chúng tôi đã dạy dạy Red Redux để nhận ra những người tạo hành động đặc biệt như thế này (chúng tôi gọi họ là người tạo hành động thunk ), giờ đây chúng tôi có thể sử dụng chúng ở bất kỳ nơi nào chúng tôi sẽ sử dụng người tạo hành động thông thường. Ví dụ: chúng ta có thể sử dụng chúng với connect()
:
// 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 trạng thái trong Thunks
Thông thường bộ giảm tốc của bạn chứa logic nghiệp vụ để xác định trạng thái tiếp theo. Tuy nhiên, bộ giảm tốc chỉ đá sau khi các hành động được gửi đi. Điều gì xảy ra nếu bạn có một tác dụng phụ (như gọi API) trong trình tạo hành động thunk và bạn muốn ngăn chặn nó trong một số điều kiện?
Không sử dụng phần mềm trung gian thunk, bạn chỉ cần thực hiện kiểm tra này bên trong thành phần:
// component.js
if (this.props.areNotificationsEnabled) {
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}
Tuy nhiên, điểm trích xuất của một người tạo hành động là tập trung logic lặp đi lặp lại này trên nhiều thành phần. May mắn thay, Redux Thunk cung cấp cho bạn một cách để đọc trạng thái hiện tại của cửa hàng Redux. Ngoài ra dispatch
, nó cũng chuyển getState
làm đối số thứ hai cho hàm bạn trả về từ trình tạo hành động thunk của bạn. Điều này cho phép thunk đọc trạng thái hiện tại của cửa hàng.
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
return function (dispatch, getState) {
// Unlike in a regular action creator, we can exit early in a thunk
// Redux doesn’t care about its return value (or lack of it)
if (!getState().areNotificationsEnabled) {
return
}
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
Đừng lạm dụng mô hình này. Sẽ tốt cho việc giải cứu các cuộc gọi API khi có sẵn dữ liệu được lưu trong bộ nhớ cache, nhưng nó không phải là nền tảng tốt để xây dựng logic kinh doanh của bạn. Nếu bạn getState()
chỉ sử dụng để gửi các điều kiện khác nhau một cách có điều kiện, thay vào đó hãy xem xét đưa logic kinh doanh vào bộ giảm tốc.
Bước tiếp theo
Bây giờ bạn đã có một trực giác cơ bản về cách hoạt động của thunks, hãy xem ví dụ Redux async sử dụng chúng.
Bạn có thể tìm thấy nhiều ví dụ trong đó thunks trả lại Lời hứa. Điều này là không bắt buộc nhưng có thể rất thuận tiện. Redux không quan tâm những gì bạn trả lại từ một thunk, nhưng nó mang lại cho bạn giá trị trả về từ đó dispatch()
. Đây là lý do tại sao bạn có thể trả lại một Lời hứa từ một thunk và chờ đợi nó hoàn thành bằng cách gọi dispatch(someThunkReturningPromise()).then(...)
.
Bạn cũng có thể chia người tạo hành động thunk phức tạp thành nhiều người tạo hành động thunk nhỏ hơn. Các dispatch
phương pháp được cung cấp bởi thunks thể chấp nhận thunks chính nó, vì vậy bạn có thể áp dụng mô hình một cách đệ quy. Một lần nữa, điều này hoạt động tốt nhất với Promise vì bạn có thể triển khai luồng điều khiển không đồng bộ trên đó.
Đối với một số ứng dụng, bạn có thể thấy mình trong tình huống yêu cầu luồng điều khiển không đồng bộ của bạn quá phức tạp để được thể hiện bằng thunks. Ví dụ: thử lại các yêu cầu không thành công, luồng ủy quyền lại bằng mã thông báo hoặc việc đưa lên từng bước có thể quá dài dòng và dễ bị lỗi khi được viết theo cách này. Trong trường hợp này, bạn có thể muốn xem xét các giải pháp luồng điều khiển không đồng bộ nâng cao hơn như Redux Saga hoặc Redux Loop . Đánh giá chúng, so sánh các ví dụ phù hợp với nhu cầu của bạn và chọn một ví dụ bạn thích nhất.
Cuối cùng, không sử dụng bất cứ thứ gì (kể cả thunks) nếu bạn không có nhu cầu thực sự cho chúng. Hãy nhớ rằng, tùy thuộc vào yêu cầu, giải pháp của bạn có thể trông đơn giản như
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Đừng đổ mồ hôi trừ khi bạn biết tại sao bạn lại làm việc này.
redux-saga
câu trả lời của tôi nếu bạn muốn thứ gì đó tốt hơn thunks. Câu trả lời muộn để bạn phải cuộn một lúc lâu trước khi thấy nó xuất hiện :) không có nghĩa là nó không đáng đọc. Đây là một phím tắt: stackoverflow.com/a/38574266/82609