Have you ever wondered why you have to use a middleware like Redux Thunk , Redux Saga ? Or do you just use it because you see tutorials online telling you to use 😜 and that's it.
Anyway, it's time for you to reconsider whether we really need a middleware or not, in this article I will analyze between writing pure no middleware and using Redux Thunk.
1. Asynchronous Dispatch
For example, a user logs in to our website, after successful login, a toast message will be displayed, after 5 seconds, that message will automatically hide.
This is the simplest way to do it in Redux
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Or like this inside connected component
this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
The only difference is that inside a connected component, usually you won't access the store directly, you'll get dispatch via prop (or hook for React Hooks). However, there is no significant difference.
If you don't want to retype when dispatch
with the same actions from different components, you can separate the action like this instead of having to dispatch
an 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)
Or if you put actions inside connect()
you would use it like this
this.props.showNotification('You just logged in.')
setTimeout(() => {
this.props.hideNotification()
}, 5000)
So far we haven't used any middleware or advanced concept.
2. Split into asynchronous action action
The above approach works well in simple cases, but you may find a few problems:
- It makes you rewrite this logic anywhere you want to show the message.
- Notifications without ID to distinguish each other, easily leading to the phenomenon of dispatch
HIDE_NOTIFICATION
1 is that all existing messages on the screen are hidden earlier than expected.
To solve these problems, we need to split into a function that only focuses on timeout logic and dispatching 2 actions. It might look like this:
// 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) {
// Base on ID to identify Hide or Display
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
Components can now use showNotificationWithTimeout
without duplicating the above logic or facing the issue of hiding notifications:
// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
Why does showNotificationWithTimeout()
have dispatch
as the first argument? Because it needs to dispatch actions to the store. Normally a component does the dispatch
but since we want an external function to do this, we need to pass dispatch
in.
If you have a singleton store exported from some module, you can import it and use dispatch
directly like this
// 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.')
This looks simpler, but we shouldn't. The main reason is because it forces the store to be a singleton. This makes it difficult to integrate into server rendering. On the server, you'll want each request to have its own store, so that different users receive a different preload data.
A singleton is also harder to test.
So we shouldn't do that, or you're sure in the future the app will be client-side only.
Go back to previous version:
// 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.')
This solved the problem with repeating logic and hiding notifications.
3. Thunk Middleware
For simple apps, the above approach seems fine. You don't need to worry about middleware if you're happy with it.
In bigger apps, you may encounter some inconvenience around it.
For example, it doesn't seem very nice when we have to propagate dispatch all over the place. This makes the separate containers and component become more complicated because any component that dispatch
an asynchronous Redux action must receive dispatch
as a prop. You can't bind actions to connect()
anymore because showNotificationWithTimeout()
is not really an action creator anymore. It does not return an object (Redux action).
Also, it's not very nice to have to remember which functions are synchronous actions like showNotification()
and which are asynchronous like showNotificationWithTimeout()
. Since they are used differently, you must also be careful or you will lead to unnecessary errors.
We need a way to show Redux that the asynchronous action creators are a special case of the action creator instead of as a completely different function.
If you are still here with me and you are also aware of the problem inside your app, welcome to use Redux Thunk middleware.
In the gist, Redux Thunk "taught" Redux to recognize these special actions.
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 is also aware of functions
store.dispatch(function (dispatch) {
// ... can dispatch many times inside
dispatch({ type: 'INCREMENT' })
dispatch({ type: 'INCREMENT' })
dispatch({ type: 'INCREMENT' })
setTimeout(() => {
// ... even asynchronous!
dispatch({ type: 'DECREMENT' })
}, 1000)
})
When this middleware is enabled, if you dispatch a function, the Redux Thunk middleware will give that function a dispatch argument. Redux Thunk also helps your reducer to only accept plain object actions.
Redux Thunk also allows us to declare showNotificationWithTimeout()
as a regular Redux action creator.
// 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)
}
}
Note that the spelling is almost the same as the previous version. However, it does not accept send
as the first argument. Instead, it returns a function as the dispatch
argument.
How do we use it in the component? Obviously, we could write like this:
// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)
We are using as a currying function and pass dispatch
in.
Looks like it's more "dumb" than the previous version.
But like I said before. If Redux Thunk middleware is enabled, whenever you dispatch
a function instead of an object, the middleware will call that function with dispatch
is passed as the first argument.
So we can do it like this
// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))
Finally, dispatch
an async action looks no different from a sync action. This is a good thing because the component doesn't care what happens inside the action, whether it's synchronous or asynchronous.
If combined with connect()
, the way we dispatch
will be even more concise.
// 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)
4. Read state in Thunk
In case you want to get the current state of the Redux store, you can pass getState
as the second argument to the function you return from the thunk action creator. This allows thunk to read the current state of the store.
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
return function (dispatch, getState) {
// Redux doesn't care what you return in thunk
if (!getState().areNotificationsEnabled) {
return
}
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
5.Return in Thunk
Redux doesn't care what you return from thunk, but it will give you the value you return from thunk after dispatch
is done. That's why you can return a Promise from thunk and wait for it until it succeeds by calling
dispatch(someThunkReturningPromise()).then(...)
6. In Summary
Don't use any middleware from Redux Thunk, Redux Saga if you really don't need them and understand what you're doing.
If your app in the future is extensible and you want to get the benefits that thunk brings like solving the problem of passing dispatch
everywhere in the component, then I recommend Use immediately and always be sure. It's very light anyway.
Thanks for reading this far, see you next time