Thunks in Redux: The Basics

What Thunks Are, What They Solve, & Other Options

Gabriel Lebec
Fullstack Academy

--

This article was born as a gist for React & Redux beginners, intended to demystify what thunks are and the motivation for using them.

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.

I’d probably let action creator return a function. If it’s a function, it’s given dispatch and the state. — Dan Abramov, responding to issue #1 in Redux.

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:

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…

What are Thunks?

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:

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:

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:

Aside: Laziness

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.

Thunks in React & Redux

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.

Fundamentals & Motivation

Redux store.dispatch expects to be applied to an "action" (object with type):

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:

Problem

However, if we have to do some async, such as an AJAX call via the axios library, a simple action creator no longer works.

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.

First Thought: Call Async Directly

We could do a store.dispatch ourselves in the async handler:

This seems ok at first glance. However, it presents several cons.

Con A: Inconsistent API

Now our components sometimes call store.dispatch(syncActionCreator()), and sometimes call doSomeAsyncThing().

  • In the latter case, it’s not obvious that we are dispatching to the store. We cannot identify Redux actions at a glance, making our app’s data flow more opaque.
  • What if we later change an action-function from sync to async, or async to sync? We have to track down and modify the call site for that function, in every single component it is used. Yuck!

What we want is a way to still use store.dispatch(actionCreator()), even for async actions.

Con B: Impurity

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.

Con C: Tight Coupling

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?

Better: Thunks (Initial Attempt)

Enter thunks. Instead of making the network call now, you return a function which can be executed at will later.

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 dispatched (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.

Redux-Thunk Middleware

The redux-thunk middleware, once installed, does essentially the following:

  • If a normal action object is dispatched, redux-thunk simply passes it along (e.g. into the reducer), as if redux-thunkdid not exist.
  • If a function (e.g. a thunk) is dispatched, 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.

Image source: http://slides.com/jenyaterpil/redux-from-twitter-hype-to-production

Dependency Injection

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:

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:

The few use cases where I think it’s acceptable is for checking cached data before you make a request, or for checking whether you are authenticated (in other words, doing a conditional dispatch)Dan Abramov

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. So if we wanted to inject axios, letting it be more easily mocked out for testing, we could.

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.

Why Thunk Middleware, and not Promise Middleware?

Promises are composable representations of asynchronous values, which have become native and widespread in JavaScript. The redux-promise and 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.)

Naive Promises Use

An initial attempt at using promises (without thunks) in Redux might appear as follows:

Look closely; this is essentially our “Call Async Directly” idea again. The promiseLogin function eventually dispatches an action from within a success handler. We are also dispatching 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:

  • Again, calling async code immediately makes our component / action-creator impure, which is difficult to work with and test.
  • Our promiseLogin is still coupled to a specific store, reducing reusability.
  • It can be tricky to distinguish a promise object from an action object. P/A+ promises have a painstaking [[promiseResolutionProcedure]] 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.

Smarter Promise Usage

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:

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 dispatching 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.

Thunked Promises

As it turns out, nothing prevents us from using redux-promise-middleware alongside thunk-middleware. By delaying the creation of the promise, we gain both the laziness of thunks and the automatic action dispatching of redux-promise-middleware:

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.

Returning Promises from Thunks

When using redux-thunk, if a dispatched thunk returns a promise then dispatch will also return that same promise:

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.

By CassioZen

Alternatives to Thunks

Thunks have caused a lot of headaches apparently.

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.

Conclusion

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.

Additional Resources

--

--