We will learn how we can generalize wrapping dispatch() for different purposes into a concept called “middleware” that is widely available in the Redux ecosystem.
[00:00] We have written two functions that wrap the dispatch function to add custom behavior. Let's take a closer look at how they work together.
[00:08] The final version of the dispatch function before we turn in the store is the result of calling add promise support to this patch.
[00:17] The function it returns acts like a normal dispatch function, but if it gets a promise, it waits for this promise to resolve and passes the result to raw dispatch, where raw dispatch is the previous value of store dispatch.
[00:33] For non-promises, it calls raw dispatch right away. Raw dispatch corresponds to the store dispatch at the time add promise support to dispatch was called.
[00:45] Store dispatch was reassigned earlier, so it's not completely fair to refer to it as raw dispatch. I'm renaming raw dispatch to next because this is the next dispatch function in the chain.
[01:00] It refers to store dispatch that was returned from add login to dispatch. Add login to dispatch also returns a function with the same API as the original dispatch function, but it logs the action type, the previous state, the action, and the next state along the way.
[01:21] It calls the raw dispatch with corresponds to store dispatch at the time add login to dispatch was called. In this case, this is the store dispatch provided by create store.
[01:34] However, it is entirely conceivable that we might want to override dispatch function before adding the login. For consistency, I'm going to rename raw dispatch to next, as well. It's just that in this particular case, next points to the original store dispatch.
[01:53] While this method of extending the store works, it's not really great at re-override the public API and replace it with custom functions. To get away from this pattern, I am declaring an array of what I'm calling middleware functions, which is just a fancy name I use for these functions I wrote, and I'm pushing add login to dispatch itself to an array, so this is an array of functions that will be applied later as a single step.
[02:22] I'm also adding add promise support to dispatch to the middleware array, and again, I don't need to apply it yet because I'll write a separate function that applies the middlewares.
[02:34] I will call this function grab dispatch with middlewares, and I'll pass this store as the first argument and an array of middlewares as the second argument.
[02:46] I'm adding this new function called grab dispatch with middlewares, and inside it I'm going to use the middlewares for each method. to run some code for every middleware.
[02:58] Specifically, I want to override this store dispatch function to point to the result of calling the middleware with the store as an argument.
[03:07] Inside the middleware functions themselves, there is a certain pattern that I have to repeat. I'm grabbing the value of store dispatch, and I'm storing it in a variable called next so that I can call it later.
[03:23] To make it a part of the middleware contract, I can make next an outside argument, just like the store before it and the action after it.
[03:34] This way, the middleware becomes a function that returns a function that returns a function, which is not very common in JavaScript, but is actually very common in functional programming languages, and this pattern is called Korean.
[03:48] Again, rather than take the next middleware from the store, I will make it intractable as an argument so that my function that calls the middlewares can choose which middleware to pass.
[04:04] Finally, store is not the only injected argument, so I also need to inject the next middleware, which is the previous value of store dispatch function.
[04:14] Now that middlewares are a first-class concept, I will rename add login to dispatch to just logger, and I will rename add promise support to dispatch to the promise middleware.
[04:31] The Korean style of function declaration can get very hard to read. However, we can use arrow functions and rely on the fact that arrow functions can have expressions as their bodies.
[04:46] It is still a function that returns a function returning a function, but it's much easier to read, and the mental model you can use for this is that this is just a function with several arguments that are applied as they become available.
[05:04] Finally, I'd like to fix the order in which middlewares are specified. They are currently specified in the order in which the dispatch function is overridden. However, it would be more natural to specify the order in which the action propagates through the middlewares.
[05:22] This is why I'm changing my middleware declaration to specify them in the order in which the action travels through them. Instead, I will grab dispatch with middlewares from right to left, so I clone the past array and I reverse it.
[05:41] Let's recap what we learned about middleware so far. Middleware can serve very different purposes. It might add some useful debugging information, such as the case with login middleware, or it might amend dispatch to understand something other than plain actions, which is the case with the promise middleware.
[06:02] We created a new function that takes the store and an array of middlewares, and does the dirty work of reassigning the store dispatch function. This lets me keep configure store a little bit more declarative, because I specified the middlewares in an array, and I call grab dispatch with middlewares to apply them.
[06:25] Let's recap how grab dispatch with middlewares works internally. The store object that the application uses contains a dispatch function. Grab dispatch with middlewares overrides it multiple times as it enumerates the middleware's array.
[06:42] However, it reverses the array before enumerating it, so the final value of store dispatch will correspond to the result of calling the first middleware in the array, which is the promise middleware.
[06:57] The promise middleware returns a dispatch function that understands promises, but to get it, we pass the store and the next dispatch function as carried arguments.
[07:12] The next dispatch function is the one that we got from the middleware that appeared earlier in the reverse array, or later in the original array, and this is the logger middleware.
[07:24] The logger middleware provided the dispatch function that logs the actions, but to get it, we passed the store and the next dispatch function to it, and it corresponded to the value store dispatch that was there before the middleware was even applied, and this is the original dispatch implementation from create store.
[07:49] The purpose of the middlewares is to replace the single dispatch function with a chain of composable dispatch functions which each can do something with an action.
[08:01] The promise middleware appears before the logger middleware in the chain, and this is why the next inside it corresponds to the dispatch function returned from the logger middleware.
[08:13] Logger middleware itself is the last middleware in chain, so its next function corresponds to the original store dispatch function, which is defined inside create store.
[08:26] Middleware is a powerful system that lets us put custom behavior before action reaches the reducers. People use middleware for different purposes, such as login, analytics, error-handling, asynchronous counterflow, and more.
According to the transcript, Dan describes the pattern of a function returning a function as 'Korean' although he almost certainly means to say 'currying.'
When you'll reach video 23, you'll get it :) I think he writes it this way, because he knows he'll need it later...
Im not sure if this was a mistake or something that I'm missing in the code that's being explained but around 6:40 you describe that the middlewares are passed in the reverse order that they are being defined but from what I can see (and I ran this through the debugger,) since the promise function is defined first and after the array is reversed, is therefore sent through last. In the video you're saying that the logger is sent last. Although this logically makes sense to me it seems as though its not functioning this way since the logger action would be undefined as demonstrated in an earlier video.
When you'll reach video 23, you'll get it :) I think he writes it this way, because he knows he'll need it later...
Heh ok thanks for expressing that. In the current examples, it seems clear that currying is unnecessary and we could just pass multiple arguments into one function. Thanks for your comment. :) I'll carry on now knowing this is going to be useful later.
what is the point of having store as the first parameter in the curry function? I didn't see anywhere you use the store in the logger or promise function.
store
is actually being used in logger
, to call getState()
so the state can be logged.
With that in mind, it makes sense to pass it to promise
as well, even though the latter doesn't use it, just so that the two functions have the same signature, and can therefore be used in wrapDispatchWithMiddlewares
.
I imagine that it's a standard pattern when writing middleware to include the store so that the middleware has the option of doing something with it.
I agree with @Sami that the currying isn't necessary in this specific example. We could just as well replace store => next =>
with (store, next) =>
, and middleware(store)(store.dispatch)
with middleware(store, store.dispatch)
.
For that matter, since we're currently passing as the second argument the dispatch
property of the first argument, we could even go back to the earlier code and not pass dispatch
as an argument at all, but rather extract it from store
within the function.
While I think the lesson on the whole is excellent, the currying strikes me as premature and unnecessarily confusing. Dan offers something like the following as a brief justification (from the transcript): "Again, rather than take the next middleware from the store, I will make it intractable as an argument so that my function that calls the middlewares can choose which middleware to pass." I was confused as much by this explanation as by the code it's explaining, but I think it means that there might be a scenario where you want to pass one store
, but then a dispatch
that is different than the one attached to that store
. Extracting dispatch
from store
within the function would no longer work for that purpose. However, I don't think that such a scenario is actually occurring in this lesson, so there's nothing to illustrate why this is worth doing.
And, even in such a scenario, you could still achieve the same end by passing both store
and whatever other dispatch
as two arguments to the same function, like the code in my first paragraph. I'm still wrapping my head around the intricacies of functional programming (and specifically functions that return functions), so there may well be some benefit of doing it with currying in this specific example that I'm not seeing. But that just further emphasizes that the technique in this lesson outstrips the need and the explanations given.
If indeed this will pay off in later lessons, as @mobility-team suggests, I would rather Dan have saved writing the code until then, when its utility could be illustrated. Or, at bare minimum, say in this lesson "don't worry about why I'm writing this this way; it will be useful later."
Im not sure if this was a mistake or something that I'm missing in the code that's being explained but around 6:40 you describe that the middlewares are passed in the reverse order that they are being defined but from what I can see (and I ran this through the debugger,) since the promise function is defined first and after the array is reversed, is therefore sent through last. In the video you're saying that the logger is sent last. Although this logically makes sense to me it seems as though its not functioning this way since the logger action would be undefined as demonstrated in an earlier video.
@Brickwork I totally relate to your confusion, because, while I believe what Dan said is correct, it's a bit mind-bending to think through. In a way, the chronology reverses itself twice(!). It works like this (three steps):
Step 1—Populate the middlewares array: We place promise
in the empty array and then logger
after it. So promise
is "defined" first in the array, and logger
last.
Step 2—Apply the middlewares to store.dispatch
: We iterate through the middlewares array, in reverse, overriding store.dispatch
with the result of calling the middleware on store
. So we call logger
first and promise
last.
However, think about what actually happens when you call each middleware on the store. It wraps whatever store.dispatch
is currently with some other code that will run beforehand. So when we call logger
first, the result is that calling store.dispatch
will run logger
's custom code, then the code of the original store.dispatch
that came with Redux. And then when we call promise
, the result is that calling store.dispatch
will run promise
's custom code, then logger
's custom code, then the code of the original store.dispatch
that came with Redux.
The critical thing to grasp here is that each successive middleware wraps the previous ones, so an entry in the array that gets applied to store
after another will run its code before the other.
Step 3—Dispatch an action: After configuring store
is complete, any action we dispatch will get passed through all the middlewares. So in our example, dispatching an action will run it through promise
first, then logger
, then Redux's default dispatch
.
This is why Dan says that the middlewares are 1) "defined" in one order (promise
added to the array before logger
) but then 2) "passed" in the reverse order (logger
applied to store.dispatch
before promise
) but then 3) "sent" in the original order (a dispatched action is processed first by promise
then by logger
)—the "double reversal" I mentioned at the top.
Why do it this way? We could have added logger
to the array first and then promise
, and then not reversed the array when applying the middleware. That would keep the middlewares "defined" in the same order that they're "passed." However, it would still be the case that, because each applied middleware wraps the previous one, an action would be "sent" (i.e. processed by the middlewares) in the reverse order, promise
before logger
.
That would only be a single reversal, which maybe is easier to understand. But then you have to remember to add the middlewares to the array in the reverse order that you want them to process actions. That is, you have to keep in mind that even though you put logger
in the array first, promise
will actually take effect first. To me this seems more confusing in the long term, since you always have to remember this. I think that's why Dan writes it the other way—yes, it's confusing to understand the code initially, but then once you do, you can just add middlewares to the array in the same order that you want them to take effect, and that's much easier to understand.
what is the point of having store as the first parameter in the curry function? I didn't see anywhere you use the store in the logger or promise function.