The whole team here is going through very rewarding times lately.
Since we’ve started working on our new shopping cart’s frontend stack, we’ve had the chance to dive deeper into some awesome modern techs.
This rewrite is entirely done in TypeScript, decoupling the theming from a freshly baked SDK.
Redux is also a central piece of this stack. My good friend Jean-Seb already covered why we are using it—today, let’s move a bit more under the hood.
I want to use our experience as an example of how to use Redux middleware. More precisely, I’ll explain how we exposed a promise-based API from a reactive core. To do so, I’ll:
Define what exactly Redux middleware is
Expose more details about our SDK
Explain how we ended up exposing a promise-based API from it
Show how we leverage Redux middleware capabilities
This article is the second chapter of our v3.0 Journal where we reveal interesting parts of our shopping cart’s rewrite. To read the entire thing:
Chapter One: How We Use Redux & Redux-Observable with VueChapter Two: Exposing a Promise-Based API from a Reactive CoreChapter Three: Template OverridingChapter Four: How We Generate Our Documentation with Nuxt & Sanity
What is Redux middleware?
Generally, middleware refers to bits of code that sit between a source and a destination, executing logic and potentially altering a given object along the way. If you worked with a web application framework in the past (like Express or Koa), chances are you've dealt with middleware.
To describe Redux’s middleware let’s refer to its official documentation:
Redux middleware solves different problems than Express or Koa middleware but in a conceptually similar way. It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
Reading the docs, you might think Redux middleware is a big monolithic piece of the puzzle. Although it technically is, think of it more as a composition of multiple middleware functions.
Redux middlewares are used for a number of reasons: logging actions, crash reporting, routing, etc. For our specific use case, as for many others, we use one to talk to an asynchronous API. We chose redux-observable to do that.
One thing you have to know about Redux is that its reducers must be synchronous. So, atomic operations as a whole must be represented by more than one action, since they happen asynchronously. You can see how it can become cumbersome for people not accustomed to using reactive patterns.
That’s why we had to think of a way to abstract all this process away from developers using our library so it could remain accessible to the vast majority of them.
I think the best way to understand how it all works is by learning from real-life examples. Let’s waste no more time and get into it!
Our Redux middleware example (or exposing a promise-based API from a reactive core)
Important definitions
Let's shed some light on a few more concepts we'll play with:
Reactivity is a paradigm where the execution of code occurs as a reaction to some other side-effect.
A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation, and its resulting value.
The actions pipeline is the term I use to designate the flow of Redux actions down the middleware stack to the reducers and back up the middleware stack again.
I also recommend reading about Redux's data flow to help you get a grasp on what follows in the rest of this post.
Challenge: using Redux without forcing it upon users
Some developers don’t know Redux. Some developers don’t even have a clue what reactivity means. Nevertheless, they should still be able to use our product and be able to customize their Snipcart integration.
So, it would be a shame having to ask them to dispatch actions to our Redux store, right?
Say you want to add an item to your cart. You expect to call something like sdk.addItem(myNewItem)
that'll return a promise. That's the standard way of doing things.
Using Redux to develop the internal plumbing of our SDK gives us flexibility in covering complex use case like debouncing and throttling.
However, the flipside of using Redux is that we do not accommodate the simple promise pattern out of the box. Actions get dispatched. Remote calls happen within redux-observable epics, which in turn dispatch new actions. The global reactive state gets mutated along the way, and all of this is entirely asynchronous.
No need to say we had to hide away this complexity as much as we could. Our goals were twofold:
Developers must be able to execute an atomic operation such as addItem
the “classic” way, i.e., calling methods that return promises
The adapter that makes the operation happen should be integrated in the most unobtrusive way possible for our team
Challenge accepted: how we achieved it
Since we're big fans of the DRY (Don’t Repeat Yourself) principle, we wanted to put something in place that would be cross-cutting over all of the actions being dispatched. Something that would hook itself into the actions pipeline and manage the whole promises thing automatically.
This is starting to resemble a middleware, right?
Atomic operation == more than one action
The first thing to understand is that operations span over more than one action dispatch. Let's get back to the addItem
I mentioned earlier. Since it's an async remote call to the Snipacrt API, we apply the async action pattern using redux-observable.
The operation is split into distinct actions: ITEM_ADD
, ITEM_ADDED
, and ITEM_ADD_FAILED
Wrapping our addItem
scenario then takes one of these two forms:
New promise created with ITEM_ADD
, and resolved with ITEM_ADDED
New promise created with ITEM_ADD
, and rejected with ITEM_ADD_FAILED
Adding context to actions
First, we need a way to tell our middleware what actions should be wrapped, and what actions should just fly by, untouched. We'll add a source
property.
We also need to indicate which subsequent action(s) will resolve the promise and which one(s) will reject it. That's what resolvesWith
and rejectsWith
arrays are for.
So now, this simple SDK call:
sdk.addItem({
id: 'eggnog',
name: 'Eggnogg carton - 2L',
price: 6.99
})
Will be dispatched like this internally:
store.dispatch({
type: 'ITEM_ADD',
source: 'SDK'
resolvesWith: ['ITEM_ADDED'],
rejectsWith: ['ITEM_ADD_FAILED']
payload: {
id: 'eggnog',
name: 'Eggnogg carton - 2L',
price: 6.99
}
})
Leveraging the Redux middleware
We call it PromiseWrapperMiddleware
. It has the responsibility of tracking promises, which happens in two stages:
1. Wrap
When an action with source:'SDK'
gets dispatched, our middleware:
Adds an identifier
property to the action
Creates a new promise
Saves the promise resolution context (callbacks and resolution rules) in a local cache with the identifier
value as the key.
Relays to the next middleware
Returns the promise to the upstream caller.
The ID we add is what ties the whole operation lifecycle together. It'll be carried over to each subsequent action dispatched as a result of the initial one.
2. Unwrap
When an action with an identifier
gets dispatched, that means it's part of a wrapped operation. Our middleware then:
Relays to the next middleware. This is important to do it first because we want the action to mutate our store's state before resolving the promise, so everything remains consistent
Retrieves the promise resolution context from the local cache using the ID
Resolves/rejects the promise if the action dispatched matches any of the resolvesWith
/rejectsWith
values
Clears the entry from the local cache
Returns to the upstream caller
Wrapping up (pun intended)
More of a visual person? Me too. Here's what the big picture looks like.
The above shows the process of adding an item, happening in 2 steps:
An addItem(...)
call is made from the public layer of the SDK. This call dispatches the ADD_ITEM
action. As it flows through the pipeline, the action gets a promise associated with it by our middleware (blue). It then continues its course down to the Redux store’s state. When the action heads back upstream, it hits the redux-observable middleware (purple), where a remote call to the backend API is fired.
When the asynchronous API call is completed, depending on the outcome, an action of type ITEM_ADDED
or ITEM_ADD_FAILED
is dispatched. When this action reaches our middleware (blue), the promise associated with the atomic addItem
operation gets resolved/rejected.
Adopting this approach makes the developer experience as smooth as possible for our SDK users, but at the same time allows us to leverage the incredible power of Redux. The best of both worlds, as they say.
I hope this resource gives you a better grasp of what middleware is and what it can be used for. Needless to say, it’s a key piece of the puzzle for us. Now, I know it’s a particular use case—there are many more ways of leveraging Redux middleware. To learn more, I recommend these resources:
If you’re interested in the concepts covered here—SDKs, Redux, TypeScript, Vue.js— I encourage you to stay in touch with our blog. Our v3.0 Journal series will be back in early 2019 with more experiments around these great tools.
As always, we’re very open to feedback. Let us know what you think in the comments!
If you've enjoyed this post, please take a second to share it on Twitter.