Setting up state management
`mobx` is the core library and `mobx-react-lite` are bindings for our react-native UI
Now that we have a basic app running, we can start developing our app.
We are going to be a little counter-intuitive here and not start with our app's UI, but rather with state management. React comes with tools to manage state without the need for third party libraries, however, I believe that React pollutes the thinking by mixing declarative UI with the need for having side-effects, and by modelling the state first with MobX and then creating the UI, we avoid a lot of confusion about the two.
MobX is a state management library based on the observer pattern. For those not familiar with the observer pattern, MobX works by creating observables. An
Observable is nothing more than an abstraction around basic values (numbers, strings, arrays, objects, etc.). Once a value has been wrapped in an
Observable, it can be "observed" by other objects and functions. When the value inside of the
observable changes, then the
observers can react to the change. In the case of functions they will automatically re-run, and in the case of React components they will re-render. This is a incredibly powerful pattern to build reactive apps.
For those native devs who are familiar with the latest SwiftUI tooling, it is similar to the Combine framework, in the sense that it allows your UI to react to changes by observing objects.
React has an integrated way of handling state and other side-effects, they are called hooks. In my opinion the problem with hooks is that they don't scale or compose when you have too many of them, or have to handle state across components. We will use some hooks in the course, but my advice is: keep them simple and only for tiny local state, and rely on MobX (or Redux or other state-management libraries) for the rest.
We are going to start in the terminal by adding MobX into our dependencies:
Just to be clear
mobx is the core library for observables and
mobx-react-lite are bindings for our React-Native UI
Setting up a root store#
We are going to organize our state by using
store is nothing more than a logical unit for grouping similar functionality and state.
All of our stores will be registered within a
root store, and every sub-store has access to the
root store in order for the different parts of the app to communicate and pass data.
We are going to start by creating a
Store.ts file in the
./src folder. This file will hold the
root store. Afterwards we will create a
UI store, which will hold data for the user interface. This is generic for now, but as your app grows you should break down your state into multiple stores.
Let's walk over the code line by line.
First we import the
createUIStore function from the
stores/UI.store.ts file. This file and this function don't exist for now, but we will create it in the next step. The
createUIStore function will do as its name describes - create an instance of a
Next we will declare the type of our root store using a TypeScript interface. Whichever object "implements" this interface must fulfil all the fields and properties declared inside of it. TypeScript can and will automatically detect the type of most of the variables you create, but there are several reasons to declare interfaces explicitly. For example, in large code bases it helps the type checker to remain fast, and it also protects you from accidentally breaking implicit dependencies in your code.
There is one problem with our code - it has a circular dependency. Because the
UIStore will have access to the
root object and the
root contains the
UIStore, the type checker will enter an infinite loop and because it cannot infer the type of the root store without determining the type of the UIStore, it can never infer the types of either. By explicitly declaring our type (i.e.
typeof createUIStore) we help the type checker break this infinite loop and correctly infer the type for our
Once the type of the root store is created, we can finally create the
createRootStore function, which will be responsible for instantiating the
First we will declare the function. Here you can see we are using our declared
IRootStoreinterface which means whatever value this function returns it must match the type definition.
Afterwards we create an empty object where we will insert the different instances of the sub-stores. We will declare it with an
anybeing the super-type of all types in TypeScript, basically a joker card of types. We do this so we can freely insert values at runtime without the type checker screaming at us.
Next we can invoke our theoretical
createUIStorefunction. We pass it the
storeobject, which will allow it to access other stores as our app grows. We then return our instance of the root store.
Finally, we instantiate an object from our root store and export it from the file so it can be imported from our React code.
Computed properties and actions#
So far we've explained observables, but have not talked about observers. Observers can be either computed properties or React components.
Computed properties are just that, properties that get computed (i.e. derived) from observables, in MobX they are just functions. Computed properties (i.e. functions) are observers of any Observable used inside of them. You don't need to worry about how this is achieved, you can just assume that if you use an Observable inside the body of the observer function, whenever any Observable changes, the function will automatically run and the result get updated.
One place where you have seen this is in Microsoft's Excel! In Excel you can declare functions inside of the cells, and whenever one of the values in the function changes, the cell value gets automatically re-calculated, without you moving a finger.
If you are interested on how exactly this is achieved here is a high level overview of the implementation, but basically a dependency tree is created based on access patterns and only re-runs the necessary observers.