Redux: Dispatching Actions Asynchronously with Thunks

Dan Abramov
InstructorDan Abramov

Share this video with your friends

Send Tweet
Published 6 years ago
Updated 3 years ago

We will learn about “thunks”—the most common way to write async action creators in Redux.

[00:00] To show the loading indicator, I dispatch the request todos action before I fetch the todos. It would be great if I could make request todos dispatched automatically when I fetch the todos because I never want to fire them separately.

[00:17] I'm removing the explicit request todos dispatch from the component, and in the file where I define the action creators, I'm no longer exporting request todos action creator.

[00:28] I want to dispatch request todos when they start fetching, and receive todos when they finish fetching, but the fetch todos action creator only resolves through the receive todos action.

[00:40] An action promise resolves through a single action at the end, but we want an abstraction that represents multiple actions dispatched over the period of time. This is why rather than return a promise, I want to return a function that accepts a dispatch callback argument.

[00:58] This lets me call dispatch as many times as I like at any point of time during the async operation. I can dispatch the request todos action in the beginning, and when the promise resolves, I can explicitly dispatch another receive todos action at the end.

[01:16] This means more typing than returning a promise, but it also gives me more flexibility. A promise can only express one async value, so fetch todos now returns a function with a callback argument so that it can call it multiple times during the async operation.

[01:35] Such functions returned from other functions are often called thunks, so we're going to implement a thunk middleware to support them. I'm opening the configure store file where I define the middleware, and I will remove the promise middleware, and I will replace it with a different middleware that I'm going to write now.

[01:56] The new middleware I'm writing is called the thunk middleware because it supports the dispatching of thunks, and it takes the store, the next middleware, and the action as current arguments, just like any other middleware.

[02:11] If the action is not an action, but rather a function, we're going to assume that this is a thunk that wants the dispatch function to be injected into it, so I'm calling the action with store dispatch. Otherwise, I'm just going to return the result of passing the action to the next middleware in chain.

[02:33] As a final step, I need to add the thunk middleware I just wrote to my array of middlewares so that it gets applied to the store. Let's recap how thunk middleware lets us dispatch multiple actions asynchronously.

[02:48] The thunk middleware has the same signature as all Redux middlewares. It accepts the store, the next dispatch function, and the action as carried arguments. It returns a new dispatch function that checks if the action is actually a function itself.

[03:09] We can see that this is the case with fetch todos action creator. Its return value is a function that wants dispatch as its argument so that it can dispatch multiple times.

[03:20] This is why when we see an action that is really a function, a thunk, we call it with store dispatch as an argument so that it can dispatch other actions by itself. The store dispatch function becomes available as the dispatch argument inside the thunk.

[03:40] Now, the thunk can use the dispatch argument to dispatch the request todos action in the very beginning, and receive todos action at the very end. The dispatch function injected into thunks is already wrapped with the whole middleware chain, so thunks can dispatch both plain object actions and other thunks.

[04:01] No matter what gets dispatched, it will go through the middleware chain again, and if its type is a function, it will be called like a thunk, but otherwise, it will be passed on to the next middleware in chain.

[04:14] In this case, it's the logger. Only plain object actions reach the logger, and then reach the reducers. The thunk middleware is a powerful, composable way to express async action creators that want to emit several actions during the course of an async operation.

[04:35] This lets the components specify the intention to start an async operation without worrying which actions get dispatched and when.

Dmitry
Dmitry
~ 6 years ago

What about redux-saga? It looks like nice alternative to thunks.

Ken Snyder
Ken Snyder
~ 5 years ago

I love thunks and use them regularly but I find myself often wanting "composable" or "thennable" thunks. Is that a thing? Let me explain what I'm looking for ... I want to call two action creators and both are thunks but they have an synchronous dependency between them (aka, B must follow A).

I'd want the code to look something like:

dispatch(actionCreator.A()) .then(dispatch(actionCreator.B())

Charlie
Charlie
~ 5 years ago

Why explicitly pass store.dispatch, rather than next, to action? For example, this is the recommended code:

const thunk = (store) => (next) => (action) => {
  (typeof action === 'function') ?
    action(store.dispatch) :
    next(action);

Is the above objectively better than:

const thunk = (store) => (next) => (action) => {
  (typeof action === 'function') ?
    action(next) : // Passing next instead of store.dispatch
    next(action);

Thanks!

Dnamic
Dnamic
~ 5 years ago

For some reason when the video gets to 2:57 the video crashes and it can't be seen.

Nino Harris
Nino Harris
~ 4 years ago

Why explicitly pass store.dispatch, rather than next, to action? For example, this is the recommended code:

We call store.dispatch to 'restart' the process of sending the action from the first middleware onwards, rather than passing it to next where the next middleware will interpret/dispatch it.

Nino Harris
Nino Harris
~ 4 years ago
J. Matthew
J. Matthew
~ 3 years ago

We call store.dispatch to 'restart' the process of sending the action from the first middleware onwards, rather than passing it to next where the next middleware will interpret/dispatch it.

I want to follow up on the question from @Charlie and this response, because the question is one I've really struggled with, and I don't think this response is quite right, though it seems like it's on the right track.

You may recall that, at the beginning of "The Middleware Chain" lesson, what we now call next was called rawDispatch, and it was not passed into the function, but rather extracted from store within the function. In the course of the lesson, Dan refactored the function to implement the standard "middleware contract" structure, which takes store, next, and action as curried arguments. next replaces the earlier rawDispatch, and by passing it as an argument separate from store, it becomes decoupled—you could use store.dispatch, but you don't have to.

I commented at the time that this doesn't offer any obvious benefit, at least in the lesson, because when that function actually gets called as one of the middlewares, store.dispatch gets passed as next! So, functionally, it's the same as before, and I didn't understand why we should bother doing this.

I talked about this with a friend who has more experience with currying, and he pointed out that there might be instances (such as unit tests on the middleware itself) where you would want to use a different function for next than whatever store.dispatch is, without actually changing store. This makes sense to me in the abstract, though it would really help me to see an example.

However, this only requires that you separate next from store as arguments, not that they be curried necessarily. As was pointed out in those previous comments, you could accomplish the same goal with
const myMiddleware = (store, next) => (action) =>
as you get with the existing pattern
const myMiddleware = (store) => (next) => (action) =>

My friend concurred with this point. The benefit of currying is that you don't have to know all your argument values at the same time. When the middleware gets called, as previously linked, it's invoked with store and then the returned function is immediately also invoked with next, in the form of store.dispatch. So there's no benefit to currying in that example. But it's conceivable that you could have a scenario in which you only want to invoke the middleware with store and then save the resultant function in a variable, to be invoked later, when you have whatever next you want to pass. Again, this make sense to me conceptually, and even agrees with the limited ways in which I have made my own use of currying, but it would be helpful to see an example of that use-case applied here.

All of this gets me to a point where I understand why it might be valuable to decouple next from store, and even to then curry next instead of passing it as a second argument to the function that takes store (though examples would be so helpful). Which brings me to the question and response I referenced at the top.

I don't think @Nino Harris is quite right to say "We call store.dispatch to 'restart' the process of sending the action from the first middleware onwards," because store.dispatch only references whatever function is currently attached to that property. As we've seen, the middleware chain reassigns the property for each new middleware, wrapping the current functionality in some additional layer of functionality.

Now, it is true that, as the lesson code is written, we're passing thunk as the first middleware, so within that function, store.dispatch happens to refer to the default dispatch function that comes with Redux. But that's external knowledge we have that results from the order we happened to pass the middlewares in; the thunk code itself shouldn't have any awareness of what store.dispatch is or whether it's already including other middlewares.

And as for the second part of the quote, "rather than passing it to next where the next middleware will interpret/dispatch it," I find this confusing because, as pointed out before, next is store.dispatch in this instance. So it doesn't actually make any functional difference as this function is used in this example whether we call action(next) as @Charlie suggested or action(store.dispatch) as the code is written. Yet the code explicitly makes the point to pass store.dispatch rather than next to action, and I just can't figure out why.

I think what @Nino Harris was trying to get at is that store.dispatch is used to refer to some prior or current state of the dispatch function, rather than whatever the next version of that function would be. And what he said about making sure that the actions the thunk is dispatching get sent through the beginning of the middleware chain rather that from some midpoint does make sense to me. But I just don't see how the code is actually doing any of that.

Based on my conversation with my friend, it seems like the only justification for this approach might be if, again, you were in some unit-test or other scenario in which you wanted to pass a different value to next than what's referenced by store.dispatch, yet it was somehow important that, within thunk, it was the latter and not the former being passed to action. I can't even begin to work through that one in my head.

I find this sort of thing very difficult to think through. Perhaps I'm missing something in the existing example. If anyone understands this well, I would really appreciate an example of using thunk in a scenario where store.dispatch and next are not the same, and it matters and is beneficial that the one is passed to action and not the other.

J. Matthew
J. Matthew
~ 3 years ago

we're passing thunk as the first middleware, so within that function, store.dispatch happens to refer to the default dispatch function that comes with Redux

Actually, my statement here isn't even correct—I remembered that the middlewares array is processed in reverse order, so logger gets applied to store.dispatch before it makes it to thunk. Thus both store.dispatch and next in the thunk code are referring to logger.

This makes even less sense to me. Dan said thunks could dispatch other thunks, but the thunk middleware wraps logger, yet the dispatch function that we're passing to action is logger. It seems like this means that the dispatch available inside fetchTodos wouldn't have that thunk middleware wrapping, so any thunk action it tried to dispatch would fail. I'm so confused. I feel like I must be missing something.

J. Matthew
J. Matthew
~ 3 years ago

I love thunks and use them regularly but I find myself often wanting "composable" or "thennable" thunks. Is that a thing? Let me explain what I'm looking for ... I want to call two action creators and both are thunks but they have an synchronous dependency between them (aka, B must follow A).

@Ken Snyder I think the code in this lesson already gives you that capability, unless I'm misunderstanding you.

The fetchTodos action creator currently returns a promise. So you can do this:

fetchTodos(filter).then(someThunk);
// e.g.
fetchTodos('active').then(todos => fetchTodos('completed'));

Alternatively, you could handle the dispatch within fetchTodos, by modifying the last line to be something like this:

.then(response => dispatch(receiveTodos(filter, response)))
.then(action => dispatch(someThunk(action)));

I've tested them both out and they work as expected.