Effective MobX Patterns (Part 3)
The previous two parts (Part 1, Part 2) focused on the fundamental building blocks of MobX. With those blocks in hand we can now start solving some real-world scenarios through the lens of MobX. This post is going to be a series of examples that applies the concepts we have seen so far.
Of course, this is not an exhaustive list but should give you a taste of the kind of mental-shift you have to make to apply the MobX lens. All of the examples have been created without the
@decorator syntax. This allows you to try this out inside Chrome Console, Node REPL or in an IDE like WebStorm that supports scratch files.
This is a long post. Sorry, no TLDR here. I have 4 examples and it should get faster and easier to read after Example 2. I think :-).
- Send analytics for important actions
- Kick off operations as part of a workflow
- Perform form validation as inputs change
- Track if all registered components have loaded
Making the shift in thinking
When you learn the theory behind some library or framework and try to apply it to your own problems, you may draw a blank initially. It happens to an average guy like me and even to the best folks out there. The writing world calls it the “Writer’s block” and in the artist’s world, it’s the “Painter’s block”.
What we need are examples from simple to complex to shape our thinking style. It is only by seeing the applications, can we start to imagine the solutions to our own problems.
For MobX, it starts by understanding the fact that you have a reactive object-graph. Some parts of the tree may depend on other parts. As the tree mutates, the connected parts will react and update to reflect the changes.
The shift in thinking is about envisioning the system at hand as a set of reactive mutations + a set of corresponding effects.
Effects can be anything that produce output as a result of the reactive change. Let’s explore a variety of real-world examples and see how we can model and express them with MobX.
Example 1: Send analytics for important actions
reaction(). We also don’t want these effects lying around after they execute. Well, that leaves us with only one option: ….
In the above code, we are simply looping over the keys in our
actionMap and setting up a
when() side-effect for each key. The side-effect will run when the tracker-function (the first argument) returns
true. After running the effect-function (second argument),
when() will auto-dispose. So there is no issue of multiple reports being sent out from the app!
For us, this looks as below:
Note that, even though I am marking the login action twice, there is no reporting happening. Perfect. That is exactly the behavior we need.
It works for two reasons:
loginflag is already true, so there is no change in value
- Also the
when()side-effect has been disposed so there is no tracking happening anymore.
Example 2: Kick off operations as part of a workflow
Note that we are storing an instance variable called
state that tracks the current and previous state of the Workflow. We are also passing the map of state->task, stored as taskMap.
The tasks for a state are only performed when you transition into the state. So we need to wait for a change on
this.state.next before we can run any side-effects (tasks). Waiting for a change indicates the use of
reaction() as it will run only when the tracked observable changes value. So our monitoring code will look like so:
The first argument to
reaction() is the tracking-function, which in this case simply returns this.state.next. When the return value of the tracking-function changes, it will trigger the effect-function. The effect-function looks at the current state, looks up the task from this.taskMap and simply invokes it.
Note that we are also passing the instance of the Workflow into the task. This can be used to transition the workflow into other states.
Interestingly, this technique of storing a simple observable, like
this.state.next and using a
reaction() to trigger side-effects, can also be used for:
- Routing via react-router
- Navigating within a presentation app
- Switching between different views based on a mode
I’ll leave it as a reader-exercise to try this out. Feel free to leave comments if you hit any road blocks.
Example 3: Perform form validation as inputs change
extendObservable() API is something we haven’t seen before. By applying it on our class instance (
this), we get an ES5 equivalent of making an
@observable class property.
Since the validation logic needs to run for the lifetime of FormData, we are going to use
autorun(). We could have used
reaction() as well but we want to run validation immediately instead of waiting for the first change.
In the above code, the
autorun() will automatically trigger anytime there is a change to the tracked observables. Note that for MobX to properly track your observables, you have to use dereferencing.
runValidation() is an async call, which is why we are returning a promise. In the example above, it does not matter but in the real-world you will probably make a server call for some special validation. When the result comes back we will set the
error observable, which will in turn update the
valid computed property.
If you have an expensive validation logic, you can even use
autorunAsync(), which has an argument to debounce the execution with some delay.
autorun()) and track the
This is the logged output:
1Valid = false 2Valid = false 3Valid = false 4Valid = false 5Valid = false 6Valid = true 7--- Form Submitted ---
autorun() runs immediately, you will see the two extra logs in the beginning, one for
instance.errors and one for
instance.valid, lines 1-2. The remaining four lines (3-6) are for each change in the field.
Each field change triggers runValidation(), which internally returns a new error object each time. This causes a change in reference for
instance.errors and then trigges our
autorun() to log the valid flag. Finally when we have set all the fields,
instance.errors becomes null (again change in reference) and that logs the final “Valid = true”.
errorsproperty and a
validcomputed property to keep track of the validity.
autorun()saves the day by tying everything together.
Example 4: Track if all registered components have loaded
load()method that returns a promise. If the promise resolves, we mark the component as loaded. If it rejects, we mark it as failed. When all of them finish loading, we will report if the entire set loaded or failed.
load()of the components will not complete in a specific order. So we need an observable array to store the
loadedstate of each component. We will also track the
reportedstate of each component.
When all components have
reported, we can notify the final
loaded state of the set of components. The below code sets up the observables.
We are back to using
extendObservable() for setting up our observable state. The
loaded computed properties track as and when the components complete their load.
mark() is our action-method to mutate the observable state.
BTW, it is recommended to use
computed properties wherever you need to derive values from your observables. Think of it as a value-producing observable. Computed values are also cached, resulting in better performance. On the other hand
reaction do not produce values. Instead they provide the imperative layer for creating side-effects.
track()method on the
Tracker. This will fire off the
load()of each component and wait for the returned Promise to resolve/reject. Based on that it will mark the load state of the component.
when() all the components have
reported, the tracker can report the final
loaded state. We use
when here since we are waiting on a condition to become true (
this.reported). The side-effect of reporting back needs to happen only once, a perfect fit for
The code below takes care of the above:
setupLogger() is not really part of the solution but is used to log the reporting. It’s a good way to know if our solution works.
And the logged output shows its working as expected. As the components report, we log the current
loaded state of each component. When all of them report,
this.reported becomes true, and we see the “All Components Loaded” message.
1first: undefined, second: undefined, third: undefined 2first: true, second: undefined, third: undefined 3first: true, second: undefined, third: true 4All Components Loaded = false 5first: true, second: false, third: true
Did the Mental Shift happen?
Hope the above set of examples gave you a taste of thinking in MobX.
Its all about side-effects on an observable data-graph.
- Design the observable state
- Setup mutating
actionmethods to change the observable state
- Put in a tracking function (
reaction) to respond to changes on the observable state
The above formula should work even for complex scenarios where you need to track something after something changes, which can result in repeat of steps 1-3.