On Flux Stores and Actions

There’s been a lot of discussion on what Flux is, the different variations, and
how the pattern can be improved upon. I’ve even blogged about Flux
here on this blog!

I’ve been doing a lot of work with React and Flux in the past month. In that time,
I learned a lot about architecture, patterns, and community best practices. I
want to share some ideas that I’ve been thinking about here.

In this post, I want to focus on Flux Stores and Actions. I will cover:

What Stores are.

How they interact with Actions.

How we can test Stores.

Action replay.

What are Stores?

In Flux, Stores are simply a place where data is read out from. More specifically,
Views within a Flux architecture will be notified of changes within Stores via
the Observer pattern, and then
query for those data in order to update their own states.

So what in the system causes Stores to update their state? The answer is Actions.

Actions

I like to think about Actions as things that have occurred in our domain. For
example, in an e-commerce system we may have Actions such as “3 Items added to Cart”,
“1 Item removed from Cart”, “Cart checked out”, etc.

Actions carry a payload with them that contain all the information needed
about the Action. For example, for ITEM_REMOVED we may have this payload:

Of course, Actions are rarely what we display in the UI. When an user is buying
books in our e-commerce system, they want to see the current state
of their shopping cart, not a history of everything they’ve added or removed.

This is where Stores come in.

Updating Stores from Actions

There is a concept called Projections. A Projection is a piece of code that
take a series of Actions and produces a transient state from them. In Flux, we
put both the Projections and the transient state within a Store.

classShoppingCartStoreextendsStore{constructor(Dispatcher){// This is the transient state.this._items=[];// Subscribe to Actions and run them through our Projections.Dispatcher.register('ITEM_ADDED',this.onItemAdded);Dispatcher.register('ITEM_REMOVED',this.onItemRemoved);}// Project item added to our transient state.onItemAdded(item){this._items.push(item);this.emit('change');}// Project item removed to our transient state.onItemRemoved(item){this._items=this._items.filter(i=>i.id!==item.id);this.emit('change');}// Our read method that Views will call.items(){returnthis._items;}}

At this point, the Store has data about what Items are in the user’s ShoppingCart.
Now imagine that research has shown that Items recently removed by the user
have a high chance to be purchased by said user.

A new requirement is added to display a list of Items removed by the user.

Notice that we still have just one ITEM_REMOVED Action. Nothing has been
changed in our Action’s payload, but we now project two states from the Action.
Furthermore, the two states are completely decoupled from one another.

In a sense, the Actions are our sources of truth within the system. The transient
states within the Stores can change over time, but Actions can neither be
updated nor destroyed – they should be immutable.

This is a nice property of Flux , and allows Stores to be tested rather easily.

Testing Stores

Let’s say we want to make sure ShoppingCartStore responds properly to three
Actions: ITEMS_LOADED, ITEMS_ADDED, and ITEMS_REMOVED. Here’s how we can
test them.

describe('ShoppingCartStore',()=>{letdispatcher,store;beforeEach(()=>{dispatcher=newDispatcher();store=newShoppingCartStore(dispatcher);});it('projects correct state from actions',()=>{// Dispatch actions.dispatcher.dispatch({id:'ITEMS_LOADED',payload:[{cartId:999,itemId:777,quantity:2}]});dispatcher.dispatch({id:'ITEM_ADDED',payload:{cartId:999,itemId:888,quantity:1}});dispatcher.dispatch({id:'ITEM_REMOVED',payload:{cartId:999,itemId:777,quantity:1}});// Based on sequence of Actions above, we assert the following.expect(store.items()).to.eql([{cartId:999,itemId:888,quantity:1},{cartId:999,itemId:777,quantity:1}]);});

We can also write unit tests for one particular Action instead of the smoke test shown above.
Unit testing is especially helpful if the business logic in our Stores are more complex.

One useful property of Stores is that given a starting state and a
sequence of Actions, we should always end up with the same resulting state
after playing the Actions. This makes testing a lot easier, and can also help us
debug our application.

Action Replay

Recall that we can re-create a Store’s transient state by replaying
all Actions up to a certain point in time. This gives us another debugging tool
under our belt.

Let’s say that we noticed a Store contains bad data, but we’re not sure how
it got to that state. If we knew its initial state, and all the Actions played
since initialization, then we can pinpoint when the state became bad by replaying
each Action and checking the state for correctness.

Persisting Actions

Practically speaking, we may want to persist our Actions and Store state
somewhere accessible for debugging purposes. This might be a bounded cached
in localStorage.

There is an idea called Event Sourcing
that describes a system where all changes to the application state is captured
in an event object. In our case, this object is our Action. Greg Young has
a good talk about CQRS and Event Sourcing
that I highly recommend watching.

I won’t go into Event Sourcing here, but I think it’s an useful tool to have.

Reproducing bugs locally

One thing I would like to try out is building a mechanism for serializing Store
states and the Action log. Then, whenever a bug is encountered in production, we
can grab these serialized objects in order to reproduce the bug locally.

CircleCI has done something similar, albeit not quite the same, with ClojureScript and Om
where they serialize the application state and rehydrate them locally.

Here’s the video from their YouTube channel.

Final thoughts

In this post, I presented some ideas that I’ve been playing around with regarding
Flux Stores. I think Flux is a powerful pattern, and can help make client-side
applications more robust and easier to reason about.

Stores aren’t terribly interesting on their own. Yet, there are some cool
concepts that can be tried with Stores within our own client-side applications.