MobX 2.2: explicit actions, controlled mutations and improved DX
MobX had an amazing few weeks! It has just hit 2000 stars on GitHub. There’s been tremendous interest from the React Amsterdam talk. In addition, MobX’s excellent out-of-the-box performance characteristics have been garnering attention from the React community.
There is a guide to increase React performance. The advanced tip will increase speed by 20 times.medium.com
And some really cool public projects have been built using MobX…
…or have just started…
And a first egghead.io lesson is published! Not one to sit idle, MobX itself has been improved as well. The latest version of MobX introduces many really cool new features. All of them suggested by the community, thanks! The new version could be considered a major release, however, there are no breaking changes… so all you get is just a boring 2.2.0 version number :-).
MobX helps manage your application by introducing four clearly distinguishable concepts:
- Observable State: where the current state of the application is stored.
- Computed values: any value that could be derived automatically from the state.
- Reactions: like computed values, but instead of producing a value they produce a side effect. Which is useful to bridge to imperative programming and manage I/O.
- Actions: they modify the state and are always triggered by some event (DOM events, websockets etc).
Actions haven’t been implemented in MobX thus far as they were considered userland code. MobX has made a point to stay un-opinionated about how you chose to write your actions.
“Just do something to the state and MobX will make sure your app respects the changes.”
With release 2.2, that principle still stands, but actions now have become an explicit concept in MobX. The action method (and decorator) may now be wrapped around any method to indicate that piece of code is intended to modify state. Action takes a function, and returns a function with exactly the same signature. So it’s just as unobtrusive as the rest of MobX. Optionally, you may provide a friendly name by passing a string as the first parameter (otherwise it takes the function or property name).
So what does this tiny action thing do? Well, for starters, action wraps your original function automatically in a transaction. This means that the out-of-the-box performance of MobX will be even better.
Secondly, actions integrate very well with the mobx-react-devtools. This means that you can trace any mutation back to the causing action with the devtools or debugger.
Third, actions are always untracked. Which means that you cannot turn them into reactions accidentally by calling them from a reaction. While most people won’t run into this issue,
action makes the distinction between actions (something that modifies the state) and reactions (the state needs to trigger a side effect automatically) very clear.
And finally, there is strict mode. Actions are an opt-in concept. You may use an action to clearly express the intent of your code, getting transactions for free; but you are in no way obliged to do so. All existing code will continue working as is. Unless strict mode is enabled using ‘useStrict(true)’.
In strict mode, MobX will throw an exception on any attempt to modify state outside an action.
This forces you to use action anywhere you intend to modify the state. In addition, all state related code will be nicely marked with either @observable, @computed, @action or one of the reaction methods (see the above listing).
The contacts-list demo project has been updated to MobX 2.2 and demonstrates the usage of action and strict mode.
Improved development tools
People are often concerned about MobX based applications becoming unwieldy in the long term, since state can be modified from anywhere it’s accessed. In practice people working with MobX don’t really experience this issue, as all derivations run synchronously in MobX. If there is unexpected behavior; the mutation that caused the current behavior is simply part of the current stack and can be found easily.
MobX 2.2 tries to address this concern. First by introducing the aforementioned strict mode that requires you to express the intent to modify state explicitly in your code. Second, the new mobx-react-devtools logger will output all of the important events that happen in MobX: all actions, transactions, reactions and state mutations will appear in the log. This makes it easy to trace all actions and state mutations back to the cause:
MobX 2.2 also introduces the concept of intercept, which offers really fine grained control over state changes. Intercept is the counterpart of the already existing observe API. The api is similar. While observe allows you to observe that changes have been made, intercept allows you to intercept changes before they are applied. A bit similar to ES6 proxy traps.
When intercepting a change you can do four things with it:
- Apply the change by returning the change object from the interceptor function
- Ignore the change by returning null
- Modify the change before returning it, for example to normalize some input
- Throw an exception to signal the user or developer that something is wrong
This gist demonstrates that nicely:
The APIs that are used by the mobx-react-devtools have been exposed. This means that you can build your own devtools, visualizations and what not! Simple register a callback with “mobx.spy(callback)” and you will get detailed information about what happens inside MobX. Building a generic time traveller on top of this is not unimaginable…
‘reaction’ is a variation of autorun. It offers more fine grained controlled over which parts of the state should trigger the side effect by using an intermediate computed value. If autorun does too much for your taste, reaction might just what you are looking for. For more details see the docs.
The MobX documentation has a fresh new page with an overview of the complete API, including all devtool utilities that weren’t documented before. Check it out!
New to MobX? Use the yeoman generator to get started quickly.
New features (API wise):
- spy (and isSpyEnabled, spyReport, spyReportStart, spyReportEnd for custom events)
- getAtom, getDebugName to obtain debug information from observable data structures
- Array splice events now also report the added collection and removedCount
- Improved debug names
- toJSON has been renamed toJS
- observable(asMap()) is now the idiomatic way to create observable maps, in favor of just map(), as it is more consistent with the rest of the api
- The effect parameter of when is now untracked
- extras.trackTransactions is deprecated, use spy instead
- untracked has been undeprecated
- extras.SimpleEventEmitter has been deprecated