How we use the Flux architecture in Delve
My name is Øystein Hallaråker and today I’m going to give you an overview of how we utilize the Flux application architecture in the Delve client.
In the Delve engineering team we focus on building for scale. Both in terms of code and developers. We want to avoid what typically happens to a software product over time:
We have found type safety and demand loading to be super important to achieve this. Equally important is having a consistent, simple architecture where the complexity of our code base does not increase as we add more features. Over the last couple of years, we have had more than 70 engineers contribute to the Delve client side code. We want any of these developers to go into any feature and feel familiar with how the code is structured and how data flows through the system. Given our requirements, Flux has been a perfect fit.
What the Flux?
Flux is the application architecture that Facebook originally used together with React. It’s not actually a framework or library but rather a pattern based on a unidirectional data flow, as shown below:
The biggest advantage of this architecture is a super simple mental model that we can apply consistently across features.
As this is just a pattern, there are several frameworks implementing the Flux architecture. We are not currently using any specific framework in Delve as we, from the start, did not want to box ourselves into a corner. In the next sections I’ll go through how we have implemented each part of the Flux architecture.
In the diagram above, the “Web API Utils” represents our client side data layer. It talks to the server, indicated by the “Web API” box. Our data layer is inspired by the great superagent library and we designed it to be super simple with a single responsibility: doing network requests and returning strongly typed responses. Important principles for us in this layer have been simplicity, uniform error handling across features, no caching, and monitoring by default. Below is an example from one of our data layer providers:
Actions are at the heart of Flux. To cause any change in the application state, an Action needs to be dispatched. In Delve, most of our Actions fall in one of two categories:
- User Actions: click, type, scroll, etc.
- Data Actions: Success or Error Action, when receiving a server response
All of our Actions implement the IAction interface:
We made an early decision that all our Actions should represent something that happened, rather than what we want to happen: “LeftNavPersonSuggestionClickAction” vs “GoToPersonPageAction.” We also wanted all our Actions to be very specific and to not reuse Actions across different user interactions with our app. We typically include the UI origin of the Action in the name. We have found that there are several advantages to this:
- Usage logging for free (We have a Logger that logs all Actions)
- Separation of concerns: When dispatching an Action, no assumptions are made about what state changes that Action will cause. Compare this to dispatching an Action named after what state change we want. What happens when we want to do another (different) thing when the Action is dispatched?
Action Creators are the glue in the Flux architecture. In Delve, all Actions are dispatched from Action Creators. Our Action Creators typically either dispatch user Actions, like a user click, or talks to our data layer and dispatches Success or Error Actions when the result comes back from the server. We decided early that we wanted each network request to result in a Success or Error Action being dispatched. We never combine the result of multiple network requests to dispatch a single Success/Error Action. Simplicity and consistency are key. The Action Creators may also talk to Stores to determine if some information is already available in the client and it may do multiple data layer requests, either in parallel or in sequence. Below is an example where a document is being added as a favorite.
In terms of dispatching Actions, we are using the Facebook Flux Dispatcher. The singleton Dispatcher is super simple: Anyone can register a callback, and when an Action is dispatched, all callbacks are invoked.
Our Flux Stores hold all application state and each Store is responsible for a single domain. The domain is typically either some model or a section of the UI. Each Store registers with the Dispatcher and listens for Actions, changes its state and emits a change event that React Components listen for. One of the great things about using TypeScript is that we get type safe Action processing in Stores:
There are a few important principles we try to follow for Stores:
- All state should be in Stores, no exceptions!
- No side-effects. Stores only change their own state
- Prefer many small, single-responsibility Stores over fewer big Stores
- No dependencies between Stores
We try to keep most our React Components dumb. In addition, we have some utilities for getting rid of the boilerplate code typically related to registering with Stores and rendering when Stores change. We also ensure that all Stores get to process an Action before Components start rendering. Have a look at BaseComponent and SmartComponent in Petar’s great webpack-react-flux starter-kit. Here’s an example from the starter kit:
We have now been through the entire Flux architecture. To sum up, we have been super happy with the Flux pattern in the Delve engineering team. Coming from an MVVM based application, the Flux pattern has made our application much easier to reason about. Partly because of the unidirectional data flow, but also because this architecture enables great separation of concerns if done correctly:
- Stores do not know about Components or other Stores. Each Store only knows its domain and related Actions.
- Most Components are «dumb» and only knows about themselves and sub-Components (only container/top level Components are smart)
- Actions do not know anything but themselves and describe what happened (not what should change in the application)
Hey, why are you guys not using Redux?
As mentioned, there are many Flux implementations, and probably the most promising framework is Redux.
Redux takes a more functional approach to the Flux architecture. In Redux there is a single store that holds the entire application state. Instead of writing traditional Flux Stores, application developers write reducers that take the current state and an Action and returns the new state without mutating any of it’s arguments.
In the Delve engineering team we love the functional concepts of Redux and we see the clear advantages of working with immutable data. However, we also love TypeScript and when prototyping we found that in order to work with immutable datatypes in TypeScript we lost some of the type safety. Additionally, our code ended up being very verbose. To sum up, we found the disadvantages outweigh the advantages, so for now we have decided to go without Redux.
That’s it. I hope you found this quick introduction on how we use Flux in Delve useful. We’d love to hear other experiences in this space and do reach out if you have questions.
And yes, I am the source of the Øystein space rule :)