We often claim caution when it comes to refactoring a tech stack.
But there always comes a time when it needs to get done.
When that happens, you want to pick the right tools:
Unless you’ve got stupid amounts of VC money, you can’t afford a complete refactoring every quarter.
For our own shopping cart v3.0 rewrite, we picked Vue.js and Redux.
Weird mix you say? Not quite! In this post, I’ll show you how and why we strapped Vue.js on top of Redux.
More specifically, I’ll cover:
What are Redux & redux-observable
Why we chose Redux (over Vuex)
How we leveraged redux-observable
How we plugged Vue.js to a reactive store
This article is the first 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
I’m thrilled to finally share some of our work on this newest version of Snipcart with you guys!
Let’s start with a bit of context.
What is that cart v3.0 you’re talking about?
In the last few months, the whole team at Snipcart has been hard at work crafting a new version of our shopping cart for developers.
Read our docs to understand how our HTLM/JS-powered cart works.
The first thing we had to settle on was the goals this revamped cart had to achieve:
Offer next level checkout & cart template customization.
Let developers use any stack—it’s been Snipcart’s promise from the start.
Create the most kickass e-commerce development UX on the market.
These pushed us to carefully select our new tech stack.
It had to enable easy customization, sure. More importantly though: it had to empower seasoned AND junior developers to get shit done without getting in their way.
We picked Vue.js for the UI. Mainly because the team loves this JS framework. But also because it includes all the building blocks needed to add template customization to our cart.
This post is a companion piece to a talk our co-founder Charles gave at VueConf Toronto. We’ll cover our Vue.js experience more deeply in upcoming v3.0 posts.
You might think the next obvious choice for state management would be Vuex.
However, our goals forced us to think outside the box, where we found Redux. It became the most logical pick for us—we’ll tell you why soon enough.
Let’s make sure we’re on the same page as to what Redux is, first.
What are Redux & redux-observable?
In a nutshell, Redux is a centralized state management library.
If what follows sounds like gibberish to you, I suggest reading Redux’s basics here.
It exposes a store
, the place where your app’s state is kept safe, which can be represented by this interface:
interface Store {
getState(): State;
subscribe(callback: (s: State) => void): void;
dispatch(action: Action): Action;
}
A state is an immutable object that gets updated by reducers
. When a change has to affect the state, we dispatch
an action
to the store, which gets handled by the reducers. Its consumers then receive a new instance of the updated state.
It’s this simple contract that allows us to use Redux with almost anything. For display purposes, only getState
and subscribe
are actually required, which makes integrating it with view libraries very straightforward.
Reducers are functions that receive the current state and actions, then return a new state. They are synchronous and cannot have any side effect:
export function cartItemAdd(state: State, action: Action): State {
const addedItem = action.payload.item;
return {
...state,
items: [...state.items, addedItem],
};
}
The crunchy part of state changes is fully isolated in the reducers and an additional mechanism, middlewares
, makes it easier to augment the store with other functionalities. To keep it simple, we can see middlewares as plugins of the store.
That’s where redux-observable fits in the puzzle: it’s a Redux middleware that leverages RxJS to allow asynchronous operations on the store.
It’s similar in a way to redux-thunk or redux-saga but with all the power of the observer pattern.
More about redux-observable later on!
Why use Redux over Vuex?
A question you might still have in the back of your mind is: “Wasn’t Vuex built to do all of this with Vue?”
Yes, it was.
But our v3.0 guidelines were strict—easy customization and dev freedom above everything else. So we’ve decided to split the cart into two parts:
A JavaScript SDK, hosting all the state management and cart logic.
A slim UI layer. We’ll first provide a default modal with great UX and easy customization. Then, developers should be able to build their own using any stack.
In that particular context:
→ Vuex became a no-go as it’s tightly coupled to Vue.js. It’d force Vue usage upon our users. Redux was different and had everything we needed.
→ Redux has a very simple interface that can be used with most frameworks. Unlike Vuex, it’s framework-agnostic and built as a standalone library. It gave us the state management library we needed for our SDK to be universal.
→ It has a mature community around it. We got to try and play with many libraries leveraging Redux. Which brings us to our final addition to the stack: redux-observable.
How to leverage redux-observable?
In redux-observables, asynchronous API calls or side effects can be achieved with help from ‘Epics’, a core concept of the library.
It’s a stream of actions from which we can handle specific actions we’re interested in and emit new ones for the reducers. We’ve simplified our epics to hide the complexity of RxJS for simple cases.
Here’s a basic example of an epic:
export const addItemEpic = createEpicFor(ADD_ITEM_ACTION, async (
action: Action,
state: State,
{ api }: Dependencies
): Promise<Action | Action[]> => {
const response = await api.post(`/cart/items`, action.payload);
if (response.ok) {
const payload = await response.json<ItemAddedResponse>();
return itemAddedAction(action, payload);
}
return await errorActionFromResponse(
response, ADD_ERROR, action);
}
The observable
part of RxJS and its streams comes in handy for debouncing—when a user does multiple operations rapidly. A user clicking to increase the quantity of an item in the cart, for instance.
It wouldn’t make sense to fire as many API requests, so with ReactiveX’s operator we can batch these actions and commit them once.
Rx is a powerful but complex beast; you can learn more about it here.
Devs familiar with Node.js Express or other server-side frameworks with a middleware system shouldn’t be lost in Redux. It’s exactly the same—code can run before and after the call to the reducers.
Epics run after the reducers which allows to react to a single action both in the reducers and the epics:
First to mark new items as being not yet saved.
Then, have an epic which will make the call to save it and dispatch a new action to mark the item as saved.
We’ve also added our own middlewares that interact with the flow of actions in the store to expose a more traditional API. Even though going with a reactive store is the right approach, we have a lot of customers that are beginners in JavaScript programming: we can’t ask them to go all in with the store and dispatch synchronous actions.
So we’ve built simple middlewares to expose an API that returns Promises
and can be used with async/await, and another one that emits events that can be subscribed to.
How we plugged Vue.js to a reactive store
With the immense communities behind both Redux and Vue, a cross-over was bound to happen.
As of now though, the choice of library is limited to:
vuejs-redux
redux-vuex
a few unmaintained ones
We chose redux-vuex
for its similarity with vuex
:
import { mapState, connect } from 'redux-vuex';
import Vue from 'vue';
import { Component } from 'vue-property-decorator';
import { sdk } from 'snipcart-sdk';
/**
* we add a call to `connect` from `redux-vuex`
* this bind our store to the Vue instance
* and allow us to map its state in our components
*/
connect({
Vue,
store: sdk.store,
});
@Component({ components: { CartItem } })
export default class Cart extends Vue {
data() {
// pretty similar to vuex isn't it? :)
return mapState({
items: state => state.cart.items,
}).call(this);
}
render(h) {
return (
<ul>
<cart-item :key="item.uniqueId"
v-for="item in items" :item="item" />
</ul>
);
}
}
As you can see, plugging Vue over Redux is effortless.
We want our base Vue theme for the new cart to be a slim layer over the Redux store. A place where all the beefy logic lives.
This way, anybody can make their own custom theme with the tech they love.
Where do we go from here?
I hope this clarifies our choice of stack for the cart’s rewrite. In all honesty, we would have loved to have this kind of resource when we started working on it. ;)
This is only the foundation of the Snipcart’s v3.0. We’ll be back with more entries on the blog about the process of building it. Template overriding, packaging & distribution of the SDK, and exposing a promise-based API around a reactive core are all subjects we want to discuss on the blog in the next few weeks.
So hang around!
For now, if you need any more info about any section of this specific post let us know in the comments below.
If you've enjoyed this post, please take a second to share it on Twitter. Got comments, questions? Hit the section below!