MobX versus Redux
In principle, MobX and Redux accomplish the same goal of providing a uni-directional data flow. The store is the central actor that manages all state changes and notifies the UI and other observers of the change in state. The mechanism to achieve that is quite different between MobX and Redux.
Redux relies on immutable state snapshots and reference-comparisons between two state snapshots to check for changes. In contrast, MobX thrives on mutable state and uses a granular notification system to track state changes. This fundamental difference in approach has implications on the Developer eXperience (DX) of using each framework. We will use the DX of building a single feature to perform a MobX versus Redux comparison.
Let's start with Redux first. Here is the list of things you have to do when working with Redux:
- Define the shape of the state tree that will be encapsulated in the store. This is normally called initialState.
- Identify all actions that can be performed to change this state tree. Each action is defined in the form { type: string, payload: any }. The type property is used to identify the action and payload is additional data that is carried along with the action. The action types are usually created as string constants and exported from a module.
- Defining raw actions every time you need to dispatch them becomes very verbose. Instead, the convention is to have an action-creator function that wraps the details of the action type and takes in the payload as an argument.
- Use the connect method to wire the React component with the store. Since every state change is notified to every component, you have to be careful to not re-render your component unnecessarily. The render should only happen when the part of the state that the component actually renders has changed (via mapStateToProps). Since every state change is notified to all connected components, it might be expensive to compute mapStateToProps every single time. To minimize these computations, it is recommended to use state selector libraries such as reselect. This increases the effort required to properly set up a performant React component. If you don't use these libraries, you have to take the onus of writing an efficient shouldComponentUpdate hook for the React component.
- Inside every reducer, you have to make sure that you are always returning a new instance of the state anytime there is a change. Note that the reducers are usually kept separate from the initialState definition and that requires going back and forth to ensure the proper state is changed in each of the reducer actions.
- Any side effect you want to perform has to be wrapped in middleware. For more complex side effects which involve async operations, it is better to rely on dedicated middleware libraries, such as redux-thunk, redux-saga, or redux-observables. Note that this also complicates how side effects are constructed and executed. Each of the previously mentioned middleware have their own conventions and terminology. Additionally, the place where an action is dispatched is not co-located with the place where the actual side effect is handled. This results in more jumping around files to construct the mental model of how a feature is put together.
- As the complexity of the feature increases, there is more fragmentation between actions, action-creators, middlewares, reducers, and initialState. Not having things co-located also increases the effort needed to develop a crisp mental model of a how a feature is put together.
In the MobX world, the developer experience is quite different. You will see more of this as we explore MobX throughout this book, but here is the top-level scoop:
- Define the observable state for the feature in a store class. The various properties that can be changed and should be observed are marked with the observable API.
- Define actions that will be needed to mutate the observable state.
- Define all of the side effects (autorun, reaction and when) within the same feature class. The co-location of actions, reactions, and the observable state keeps the mental model crisp. MobX also supports async state updates out of the box, so no additional middleware libraries are needed to manage it.
- Use the mobx-react package that includes the observer API, which allows the React components to connect to the observable store. You can sprinkle observer components throughout your React component tree, which is in fact the recommended approach to fine-tune component updates.
- The advantage of using the observer is that there is no extra work needed to make the component efficient. Internally, the observer API ensures that the component is only updated when the rendered observable state changes.
MobX shifts your mindset to think of the observable state and the corresponding React components. You don't have to focus much on the wiring needed to achieve this. It is abstracted away behind simple and elegant APIs, such as observable, action, autorun, and observer.
We can go as far as saying that MobX enables a more declarative form of Redux. There are no action creators, reducers, or middleware to handle actions and produce the new state. The actions, side effects (reactions), and observable state are co-located inside the class or module. There are no complex connect() methods to glue a React component to the store. A simple observer() does the job with no extra wiring.