Redux was created by Dan Abramov for a talk. It is a “state container” inspired by the unidirectional Flux data flow and functional Elm architecture. It provides a predictable approach to managing state that benefits from immutability, keeps business logic contained, acts as the single source of truth, and has a very small API.
The synchronous and pure flow of data through Redux’s components is well-defined with distinct, simple roles. Action creators create objects → objects are dispatched to the store → the store invokes reducers → reducers generate new state → listeners are notified of state updates.
However, Redux is not an application framework, and does not dictate how effects should be handled. For that, developers can adopt any preferred strategy through middleware.
Redux-Thunk is arguably the most primitive such middleware. It is certainly the first that most people learn, having been written by Dan Abramov as part of Redux proper before being split out into a separate package. That original implementation is tiny enough to quote in its entirety:
export default function thunkMiddleware({ dispatch, getState }) {
return next => action =>
typeof action === 'function' ?
action(dispatch, getState) :
next(action);
}
Since then, the Redux-Thunk source code has only expanded to fourteen lines total. Despite this apparent simplicity, however, thunks still engender occasional confusion. If you find the concept fuzzy, fear not! We shall begin by answering a common question…
The precise definition of a “thunk” varies across contexts. Generally though, thunks are a functional programming technique used to delay computation. Instead of performing some work now, you produce a function body or unevaluated expression (the “thunk”) which can optionally be used to perform the work later. Compare:
// 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!'
Named functions help to highlight the thunk, but the distinction is made clearer using arrows. Notice how a thunk (the function returned from thunkedYell(…)
) requires an extra invocation before the work is executed:
const yell = text => console.log(text + '!')
const thunkedYell = text => () => console.log(text + '!')
// \___________________________/
// |
// the thunk
Here the potential work involves a side effect (logging), but thunks can also wrap calculations that might be slow, or even unending. In any case, other code can subsequently decide whether to actually run the thunk:
const generateReport = thunk =>
FEELING_LAZY
? `Sorry, the bean counters are asleep.`
: `You have ${thunk()} beans in your account.`
// imagine `countAllTheBeans` is a slow function:
const report = generateReport(countAllTheBeans)
Lazy languages like Haskell treat function arguments as thunks automatically, allowing for “infinite” computed-on-demand lists and clever compiler optimizations. Laziness is a powerful technique which can be implemented in JavaScript via various approaches and language features, including getters, proxies, and generators. For example, the Chalk library uses a getter to lazily build infinite property chains: chalk.dim.red.underline.bgBlue
etc. On the mathematical front, there is a thunked version of the famous Y combinator, called (aptly enough) the Z combinator, which can be run in eager languages like JavaScript.
Laziness is a large topic deserving its own article. Rather than explore thunks and laziness in general, the remainder of this post will focus on [redux-thunk](https://github.com/gaearon/redux-thunk)
.
In React / Redux, thunks enable us to avoid directly causing side effects in our actions, action creators, or components. Instead, anything impure will be wrapped in a thunk. Later, that thunk will be invoked by middleware to actually cause the effect. By transferring our side effects to running at a single point of the Redux loop (at the middleware level), the rest of our app stays relatively pure. Pure functions and components are easier to reason about, test, maintain, extend, and reuse.
Redux store.dispatch
expects to be applied to an “action” (object with type
):
const LOGIN = 'LOGIN'
store.dispatch({ type: LOGIN, user: {name: 'Lady GaGa'} })
Because manually writing action objects in multiple places is a potential source of errors (what if we accidentally wrote userr
instead of user
?), we prefer “action creator” functions that always return a correctly-formatted action:
// in an action creator module
const login = user => ({ type: LOGIN, user })
// in some component
store.dispatch(login({ name: 'Lady GaGa' })) // still dispatching an action object
However, if we have to do some async, such as an AJAX call via the axios library, a simple action creator no longer works.
// in an action creator module:
const asyncLogin = () =>
axios.get('/api/auth/me')
.then(res => res.data)
.then(user => {
// how do we use this user object?
})
// somewhere in component:
store.dispatch(asyncLogin()) // nope; `asyncLogin()` is a promise, not action
The problem is that asyncLogin
no longer returns an action object. How could it? The payload data (user object) isn’t available yet. Redux (specifically, dispatch
) doesn’t know how to handle promises – at least, not on its own.
We could do a store.dispatch
ourselves in the async handler:
// in an action creator module:
import store from '../store'
const simpleLogin = user => ({ type: LOGIN, user })
const asyncLogin = () =>
axios.get('/api/auth/me')
.then(res => res.data)
.then(user => {
store.dispatch(simpleLogin(user))
})
// somewhere in component:
asyncLogin()
This seems ok at first glance. However, it presents several cons.
Now our components sometimes call store.dispatch(syncActionCreator())
, and sometimes call doSomeAsyncThing()
.
What we want is a way to still use store.dispatch(actionCreator())
, even for async actions.
The asyncLogin
function isn’t pure; it has a side effect (network call). Of course eventually we must make that call, and we’ll see a solution soon. But side effects embedded in a component make that component harder to work with and reason about. For example, in unit testing, you may have to intercept or modify axios
otherwise the component will make actual network calls.
The asyncLogin
function is tightly coupled to a specific store
in scope. That isn’t reusable; what if we wanted to use this action creator with more than one Redux store, e.g. for server-side rendering? Or no real store at all, e.g. using a mock for testing?
Enter thunks. Instead of making the network call now, you return a function which can be executed at will later.
// in an action creator module:
import store from '../store' // still coupled (for now...)
const simpleLogin = user => ({ type: LOGIN, user })
const thunkedLogin = () => // action creator, when invoked…
() => // …returns a thunk, which when invoked…
axios.get('/api/auth/me') // …performs the actual effect.
.then(res => res.data)
.then(user => {
store.dispatch(simpleLogin(user))
})
// somewhere in component:
store.dispatch(thunkedLogin()) // dispatches the thunk to the store.
// The thunk itself (`() => axios.get…`) has not yet been called.
We’re back to a single API style, and our action creator thunkedLogin
is pure, or at least “purer”: when invoked, it returns a function, performing no immediate side effect.
“But wait,” an astute reader might object. “That action creator returns a function, which subsequently gets _dispatch_
ed (last line). I thought Redux only understands action objects? Also, this is still tightly coupled!"
Correct. If this was our only change, the thunk would be passed into our Redux reducers, uselessly. Thunks are not magic, and insufficient on their own. Some additional code will need to actually invoke the thunk. That brings us to our next tool: whenever a value gets dispatched to the Redux store, it first passes through middleware.
The redux-thunk
middleware, once installed, does essentially the following:
actionOrThunk =>
typeof actionOrThunk === 'function'
? actionOrThunk(dispatch, getState)
: passAlong(actionOrThunk);
redux-thunk
simply passes it along (e.g. into the reducer), as if redux-thunk
did not exist.redux-thunk
calls that function, passing in the store’s dispatch
and getState
. It does not forward the thunk to the reducer.Just what we needed! Now our action creators can return objects or functions. In the former case, everything works as normal. In the latter case, the function is intercepted and invoked.
When our example thunk is invoked by the middleware, it performs an asynchronous effect. _When that async is complete,_the callback or handler can dispatch a normal action to the store. Thunks therefore let us “escape” the normal Redux loop temporarily, with an async handler eventually re-entering the loop.
We have seen that thunks in Redux let us use a unified API and keep our action creators pure. However, our demonstration still used a specific store
. The redux-thunk
middleware gives us a way to solve that issue: dependency injection. DI is one technique for mitigating code coupling; instead of code knowing how to pull in a dependency (and therefore being tightly coupled to it), the dependency is provided to the code (and can therefore be easily swapped). This role reversal is an example of the more general concept of inversion of control.
Thunks generally take no arguments — they are latent computations, ready to be performed with no further input. However, redux-thunk
bends that rule, and actually passes two arguments to the thunk: dispatch
and getState
. Our standard pattern for defining thunked action creators will therefore not need a scoped store
:
// in an action creator module:
const simpleLogin = user => ({ type: LOGIN, user })
// Look, no store import!
const thunkedLogin = () => // action creator, when invoked…
dispatch => // …returns thunk; when invoked with `dispatch`…
axios.get('/api/auth/me') // …performs the actual effect.
.then(res => res.data)
.then(user => {
dispatch(simpleLogin(user))
})
// somewhere in component:
store.dispatch(thunkedLogin()) // dispatches the thunk to the store.
// The thunk itself (`dispatch => axios.get…`) has not yet been called.
// When it reaches the middleware, `redux-thunk` will intercept & invoke it,
// passing in the store's `dispatch`.
How does this work? Where does this new dispatch
argument come from?
The short answer is that the redux-thunk
middleware has access to the store, and can therefore pass in the store’s dispatch
and getState
when invoking the thunk. The middleware itself is responsible for injecting those dependencies into the thunk. The action creator module does not need to retrieve the store manually, so this action creator can be used for different stores or even a mocked dispatch
.
getState
We did not show using getState
in the thunk, as it is easy to abuse. In most Redux apps, it is more properly the responsibility of reducers (not actions) to use previous state to determine new state. There may be some cases in which reading the state inside a thunk is defensible, however, so be aware it is an option. Dan Abramov addresses using state in action creators:
withExtraArgument
“But wait, there’s more!” Not only does Redux-Thunk inject dispatch
and getState
, it can also inject any custom dependencies you want, using [withExtraArgument](https://github.com/gaearon/redux-thunk#injecting-a-custom-argument)
. So if we wanted to inject axios
, letting it be more easily mocked out for testing, we could.
// in store instantiation module:
import axios from 'axios'
const store = createStore(
reducer,
applyMiddleware(thunk.withExtraArgument(axios))
)
// in action creator module:
const thunkedLogin = () =>
(dispatch, getState, axios) => // thunk now also receives `axios` dep.
axios.get('/api/auth/me')
.then(res => res.data)
.then(user => {
dispatch(simpleLogin(user))
})
At a certain point, one wonders where the dependency injection should stop. Is no code allowed to pull in dependencies? Is there a better way? DI and IoC are useful, but perhaps not ideal. Again, be aware of the option, but consider whether it is truly necessary for your application.
Promises are composable representations of asynchronous values, which have become native and widespread in JavaScript. The [redux-promise](https://github.com/acdlite/redux-promise)
and [redux-promise-middleware](https://github.com/pburtchaell/redux-promise-middleware)
packages enable dispatching promises or action objects containing promises. Both have some good capabilities and make handling async in Redux somewhat more convenient. However, neither addresses the issue of impurity. Promises are eager; they represent an async action that has already been initiated (in contrast to a Task, which is like a lazy Promise – user code is not actually executed unless you call run
.)
An initial attempt at using promises (without thunks) in Redux might appear as follows:
// in an action creator module:
import store from '../store'
const simpleLogin = user => ({ type: LOGIN, user })
const promiseLogin = () => // action creator…
axios.get('/api/auth/me') // …returns a promise.
.then(res => res.data)
.then(user => {
store.dispatch(simpleLogin(user))
})
// somewhere in component:
store.dispatch(promiseLogin()) // Nope, still not good
Look closely; this is essentially our “Call Async Directly” idea again. The promiseLogin
function eventually dispatch
es an action from within a success handler. We are also dispatch
ing the initial promise to the store, but what would any potential middleware do with that promise? At best, we’d want a hypothetical redux-promise-naive
middleware to discard the promise so it doesn’t end up in the reducer. That’s doable, but overlooks some issues:
promiseLogin
is still coupled to a specific store
, reducing reusability.[[[promiseResolutionProcedure]]](https://promisesaplus.com/#the-promise-resolution-procedure)
for duck-typing promises safely. The foolproof way to deal with this uncertainty is to coerce values using Promise.resolve
, but doing so for every Redux action is a bit heavy-handed.The real redux-promise
and redux-promise-middleware
packages are smarter than our hypothetical redux-promise-naive
. They allow dispatching promises or actions with promise payloads, and when the promise is fulfilled, the middleware will dispatch a normal action. For example:
// with `redux-promise-middleware` active:
const promiseLogin = () => ({
type: 'LOGIN',
payload: {
promise: axios.get('/api/auth/me').then(res => res.data)
}
})
// somewhere in component:
store.dispatch(promiseLogin());
Here, redux-promise-middleware
will detect the explicitly declared payload.promise
in a dispatched action. It prevents this action from going directly to the reducer, and automatically dispatches a separate 'LOGIN_PENDING'
action instead. It then waits for the promise to settle, at which point it dispatches a 'LOGIN_FULFILLED'
or 'LOGIN_REJECTED'
action with the payload replaced by the promise’s value or reason. That’s a nice mix of actions we get for free, facilitating UI features like loading spinners or error notifications.
This middleware offers one improvement: promiseLogin
no longer depends on a particular store
. Rather, the middleware takes care of dispatch
ing the final data to the store itself.
Unfortunately, redux-promise-middleware
still hasn’t contained the side effect; promiseLogin
makes a network call immediately. This is arguably the Achilles’ heel of promise-based redux middleware, and our components are back to being impure and needing some kind of hook or modification for testing purposes or reuse in other contexts.
As it turns out, nothing prevents us from using [redux-promise-middleware](https://github.com/pburtchaell/redux-promise-middleware/blob/master/docs/guides/chaining-actions.md)
alongside [thunk-middleware](https://github.com/pburtchaell/redux-promise-middleware/blob/master/docs/guides/chaining-actions.md)
. By delaying the creation of the promise, we gain both the laziness of thunks and the automatic action dispatching of redux-promise-middleware
:
const thunkedPromiseLogin = () => // instead of returning an action…
dispatch => // …returns a thunk, which when invoked…
dispatch({ // …dispatches an action…
type: 'LOGIN',
payload: {
promise: axios.get('/api/auth/me').then(r => r.data) // …with an effect.
}
})
// somewhere in component:
store.dispatch(thunkedPromiseLogin())
At this point, the simple core concept of thunks is beginning to be buried under complexities of debatable necessity. Do we really need two middleware libraries and to remember to use specific code patterns just to handle effects in Redux? We will examine a few alternatives shortly. Before then, there is one last note we ought to cover regarding promises and thunks.
When using redux-thunk
, if a dispatched thunk returns a promise then dispatch
will also return that same promise:
const thunkedLogin = () =>
dispatch =>
axios.get('/api/auth/me')
.then(res => res.data)
.then(user => {
dispatch(simpleLogin(user))
})
// somewhere in component:
store.dispatch(thunkedLogin())
.then(() => console.log('async from component A fulfilled'))
Once again, it is easy to abuse this pattern. One generally seeks to keep React components as pure as possible; adding async handlers back into them feels like a step backwards. It also makes our API inconsistent again.
However, there are a number of times and places where using a returned promise from a dispatch
call can be nice. CassioZen presents a few in his ReactCasts #10: Redux Thunk Tricks video.
So, are thunks the One True Way to manage async / side effects in Redux applications? Certainly not. We already mentioned promise-based middleware. Thunks have at least one benefit over promises, but the below packages may be more convenient for certain use cases.
Also, thunks are among the simplest of approaches. For any more complex asynchronicity, thunks can result in a lot of manual callback logic. More sophisticated, expressive, and composable solutions exist. The most established at this time, from most to least used, are:
Redux-Saga uses generators, a native feature all JavaScript developers ought to master. The remainder of Redux-Saga’s API is ad-hoc / unique, so while you may pick it up quickly, the knowledge is not as portable. It is especially nice for testing, however, as sagas return simple descriptions of desired effects, instead of functions which perform those effects.
In comparison, Redux-Observable is built on RxJS, a large library with a longer learning curve. However, RxJS is useful outside of Redux-Observable as a powerful and composable way to manage asynchronicity.
Redux-Loop is not as widespread, but follows Redux itself in being inspired by Elm. It is interesting for focusing not on action creators but rather reducers; this keep the state logic more central and contained.
These and other considerations make choosing between sagas, observables, loops, and any other options a matter of use case and preference, with no universal winner.
Ultimately, thunks are an effective solution for applications with simple async requirements. Understanding thunks is an approachable goal for those learning Redux the first time. Once you become comfortable with them, it’s a good idea to try alternatives.
#redux #reactjs