A short story of state management
Although in small applications storing state on component-level might be enough, it's not so simple when working with growing amount of sources of data. A state can be derived from server response, Local Storage, or it can be a result of some user interaction like changing a route or filling out a form. That state might be a concern of more than one hardly related component.
This caused state containers to be created with one purpose in mind - storing all global application state in a place, where it can be easily accessed and predictably modified. First, there was Flux architecture - it was more of a proof of concept than a real solution since it was complicated and had a steep learning curve. Now we have two competing solutions, Redux and MobX, and even though the first one had a quick start, MobX is gaining popularity and has dedicated contributors.
The three principles of Redux
Redux is known for being a small and intuitive implementation of Flux architecture. It's based on hot concepts in developers' world, such as Functional Programming and Immutability. There are three principles of Redux:
- store as a single source of truth, which means that all of the application data is stored inside of a state tree. It also allows for easier debugging because where all of the information comes from;
- store is read-only, which means it cannot be changed directly but only through dispatching actions that represent a change in your application ecosystem;
- changes are made using pure functions. Each dispatch action flows through the set of reducers - pure functions that take the previous part of a state and an action with optional payload to create a new state. From the definition of pure function emerges a rule that reducers cannot have such side effects as an asynchronous request or writing to Local Storage. They also cannot mutate element of the state and have to create a new one that contains previous element's data.
When combined, those principles create a way for introducing a change in application state that is easy to reason about, debug, and extend. However, this architecture gets quite complex as an application grows.
What MobX has to offer?
MobX represents a way that differs from Redux. It draws from Object Oriented Programming and is an implementation of the Observer Pattern. It's also all about mutating state, tracking those mutations, and automatically updating all component interested in those changes. As opposed to Redux, there is more than one store in MobX. In fact, you may have as many stores as you want, starting with at least two. The first necessary one is the domain data (i.e. products sold in an online shop), while the second is for the UI state (i.e. whether modal is open or drop down menu extended).
Stores are just simple objects in which we mark some properties as observable, so MobX can start tracking their reference. This results in the lack of need for an explicit declaration on which properties we observe - MobX will figure this out by running a fragment of code and registering which properties were used during the execution. Next time those properties are changed, it will rerun the code fragment, what will prevent unnecessary updates in case of changes in the unused properties.
Although MobX allows changing the store from anywhere in your application, it still encourages using actions, especially when introducing said changes. These are not pure functions, but there is something similar between them and reducers in Redux. An action has a store in its context and takes optional parameters to introduce changes in this store. This way side effects in action are limited to changes in the store it’s associated with.
Since values are mutated, there is no need to normalise the data. Many APIs return nested objects, such as posts with nested comments. Since the data that's structured that way is very problematic for Redux, it imposes recreating this structure every time nested element changes.
Normalization is a process of creating separated, flat structures for nested objects. In the already used example it would mean a separate list of posts and comments. This however complicates things when an application is supposed to display only comments of one of the posts (since it has to filter them).
MobX way of mutating data pays up when we have complex relationships between objects in the application that would be hard to recreate in Redux. This solution is simply faster and has a much more intuitive approach.
Why you should try MobX in your next project
Setting up Redux in your project, although intuitive, can take some time as you'll have to work with a lot of boilerplate code. You'll need to create reducers, actions creators, and wire store with some third-party middlewares (i.e. redux-thunk, redux-saga) for your state container to be able to work properly. With MobX the setup couldn't be easier - it's based on writing a simple class with observable properties and passing it down to your components tree.
When it comes to Redux there are no out-of-the-box mechanisms for asynchronous action. It requires you to use middlewares with thunks or sagas. MobX, on the other hand, comes with batteries included - asynchronous actions are as simple and intuitive as they should be.
With side effects in actions, one might be worried about the testability, but there are no problems with testing MobX stores. You simply initialize them, perform an action, and then make assertions about store properties (since they are the only things that change).
The greatest MobX advantage is the lack of structural complexity and the guarantee that once a change in observable is made, every component that's using the modified data will update. This allows teams to focus on introducing new functionalities with minimal effort spent on extending the state container, which results in a rapid growth of the project. Additionally, with its unique approach, MobX will allow you to design your data structures in a way that fits your domain best and without any restraints.