
This article was peer reviewed by Michel Weststrate and Aaron Boyer. Thanks to all of SitePointâs peer reviewers for making SitePoint content the best it can be!
If youâve ever written anything more than a very simple app with jQuery, youâve probably run into the problem of keeping different parts of the UI synchronized. Often, changes to the data need to be reflected in multiple locations, and as the app grows you can find yourself tied in knots. To tame the madness, itâs common to use events to let different parts of the app know when something has changed.
So how do you manage the state of your application today? Iâm going to go out on a limb and say that youâre over subscribing to changes. Thatâs right. I donât even know you and Iâm going to call you out. If youâre not over subscribing, then Iâm SURE youâre working too hard.
Unless youâre using MobX of courseâĶ
Key Takeaways
- Leverage MobX for Simplified State Management: Utilize MobX to efficiently manage application state through observable objects, reducing the complexity and boilerplate code found in other state management libraries like Redux.
- Automatic Updates with MobX: Implement MobXâs `autorun` feature to automatically update UI components in response to state changes without manual event handling, streamlining the synchronization process across your application.
- Enhanced Performance with Computed Values: Use MobXâs computed values to derive data from the state, ensuring that components only re-render when necessary and improving overall application performance.
- Ease of Starting with MobX: Integrate MobX into existing JavaScript applications seamlessly by converting standard objects into observables, allowing for gradual adoption without a complete rewrite.
- Utilize MobX Actions for Transactional Modifications: Apply MobX actions to encapsulate state modifications in transactions, thereby batching updates and minimizing redundant renders for more efficient, bug-resistant code.
What is âStateâ Anyway?
Hereâs a person. Hey, thatâs me! I have a firstName, lastName and age.
In addition, the fullName() function might come out if Iâm in trouble.
var person = {
firstName: 'Matt',
lastName: 'Ruby',
age: 37,
fullName: function () {
this.firstName + ' ' + this.lastName;
}
};
How would you notify your various outputs (view, server, debug log) of modifications to that person? When would you trigger those notifications? Before MobX, I would use setters that would trigger custom jQuery events or js-signals. These options served me well, however, my usage of them was far from granular. I would fire one âchangedâ event if any part of the person object changed.
Letâs say I have a piece of view code that shows my first name. If I changed my age, that view would update as it was tied to that personâs changed event.
person.events = {};
person.setData = function (data) {
$.extend(person, data);
$(person.events).trigger('changed');
};
$(person.events).on('changed', function () {
console.log('first name: ' + person.firstName);
});
person.setData({age: 38});
How could we tighten that over-fire up? Easy. Just have a setter for each field and separate events for each change. Waitâwith that you may start over-firing if you wanted to change both age and firstName at once. Youâd have to create a way to delay your events from firing until both changes completed. That sounds like work and Iâm lazyâĶ
MobX to the rescue
MobX is a simple, focused, performant and unobtrusive state management library developed by Michel Weststrate.
From the MobX docs:
Just do something to the state and MobX will make sure your app respects the changes.
var person = mobx.observable({
firstName: 'Matt',
lastName: 'Ruby',
age: 37,
fullName: function () {
this.firstName + ' ' + this.lastName;
}
});
Notice the difference? mobx.observable is the only change Iâve made.
Letâs look at that console.log example again:
mobx.autorun(function () {
console.log('first name: ' + person.firstName);
});
person.age = 38; // prints nothing
person.lastName = 'RUBY!'; // still nothing
person.firstName = 'Matthew!'; // that one fired
Using autorun, MobX will only observe what has been accessed.
If you think that was neat, check this out:
mobx.autorun(function () {
console.log('Full name: ' + person.fullName);
});
person.age = 38; // print's nothing
person.lastName = 'RUBY!'; // Fires
person.firstName = 'Matthew!'; // Also fires
Intrigued? I know you are.
Core MobX concepts
observable
var log = function(data) {
$('#output').append('<pre>' +data+ '</pre>');
}
var person = mobx.observable({
firstName: 'Matt',
lastName: 'Ruby',
age: 34
});
log(person.firstName);
person.firstName = 'Mike';
log(person.firstName);
person.firstName = 'Lissy';
log(person.firstName);
MobX observable objects are just objects. Iâm not observing anything in this example. This example shows how you could start working MobX into your existing codebase. Just use mobx.observable() or mobx.extendObservable() to get started.
autorun
var person = mobx.observable({
firstName: 'Matt',
lastName: 'Ruby',
age: 0
});
mobx.autorun(function () {
log(person.firstName + ' ' + person.age);
});
// this will print Matt NN 10 times
_.times(10, function () {
person.age = _.random(40);
});
// this will print nothing
_.times(10, function () {
person.lastName = _.random(40);
});
You want to do something when your observable values change, right? Allow me to introduce autorun(), which will trigger the callback whenever a referenced observable changes. Notice in the above example how autorun() will not fire when age is changed.
computed
var person = mobx.observable({
firstName: 'Matt',
lastName: 'Ruby',
age: 0,
get fullName () {
return this.firstName + ' ' + this.lastName;
}
});
log(person.fullName);
person.firstName = 'Mike';
log(person.fullName);
person.firstName = 'Lissy';
log(person.fullName);
See that fullName function and notice how it takes no parameters and the get? MobX will automatically create a computed value for you. This is one of my favorite MobX features. Notice anything weird about person.fullName? Look again. Thatâs a function and youâre seeing the results without calling it! Normally, you would call person.fullName() not person.fullName. Youâve just met your first JS getter.
The fun doesnât end there! MobX will watch your computed valueâs dependencies for changes and only run when they have changed. If nothing has changed, a cached value will be returned. See the case below:
var person = mobx.observable({
firstName: 'Matt',
lastName: 'Ruby',
age: 0,
get fullName () {
// Note how this computed value is cached.
// We only hit this function 3 times.
log('-- hit fullName --');
return this.firstName + ' ' + this.lastName;
}
});
mobx.autorun(function () {
log(person.fullName + ' ' + person.age);
});
// this will print Matt Ruby NN 10 times
_.times(10, function () {
person.age = _.random(40);
});
person.firstName = 'Mike';
person.firstName = 'Lissy';
Here you can see that Iâve hit the person.fullName computed many times, but the only time the function is run is when either firstName or lastName are changed. This is one of the ways that MobX can greatly speed up your application.
MORE!
Iâm not going to continue re-writing MobXâs terrific documentation any longer. Look over the docs for more ways to work with and create observables.
Putting MobX to work
Before I bore you too much, letâs build something.
Hereâs a simple non-MobX example of a person that will print the personâs full name whenever the person changes.
See the Pen Simple MobX jQuery example by SitePoint (@SitePoint) on CodePen.
Notice how the name is rendered 10 times even though we never changed the first or last names. You could optimize this with many events, or checking some sort of changed payload. Thatâs way too much work.
Hereâs the same example built using MobX:
See the Pen Simple MobX jQuery example by SitePoint (@SitePoint) on CodePen.
Notice how thereâs no events, trigger or on. With MobX youâre dealing with the latest value and the fact that it has changed. Notice how it has only rendered once? Thatâs because I didnât change anything that the autorun was watching.
Letâs build something slightly less trivial:
// observable person
var person = mobx.observable({
firstName: 'Matt',
lastName: 'Ruby',
age: 37
});
// reduce the person to simple html
var printObject = function(objectToPrint) {
return _.reduce(objectToPrint, function(result, value, key) {
result += key + ': ' + value + '<br/>';
return result;
}, '');
};
// print out the person anytime there's a change
mobx.autorun(function(){
$('#person').html(printObject(person));
});
// watch all the input for changes and update the person
// object accordingly.
$('input').on('keyup', function(event) {
person[event.target.name] = $(this).val();
});
Here weâre able to edit the whole person object and watch the data output automatically. Now there are several soft spots in this example, most notably that the input values are not in sync with the person object. Letâs fix that:
mobx.autorun(function(){
$('#person').html(printObject(person));
// update the input values
_.forIn(person, function(value, key) {
$('input[name="'+key+'"]').val(value);
});
});
I know, you have one more gripe: âRuby, youâre over rendering!â Youâre right. What youâre seeing here is why many people have chosen to use React. React allows you to easily break your output into small components that can be rendered individually.
For completeness sake, hereâs a jQuery example that Iâve optimized.
Would I do something like this in a real app? Probably not. Iâd use React any day if I needed this level of granularity. When Iâve used MobX and jQuery in real applications, I use autorun()s that are granular enough that Iâm not re-building the whole DOM on every change.
Youâve made it this far, so hereâs the same example built with React and MobX
Letâs Build a Slideshow
How would you go about representing the state of a slideshow?
Letâs start with the individual slide factory:
var slideModelFactory = function (text, active) {
// id is not observable
var slide = {
id: _.uniqueId('slide_')
};
return mobx.extendObservable(slide, {
// observable fields
active: active || false,
imageText: text,
// computed
get imageMain() {
return 'https://placeholdit.imgix.net/~text?txtsize=33&txt=' + slide.imageText + '&w=350&h=150';
},
get imageThumb() {
return 'https://placeholdit.imgix.net/~text?txtsize=22&txt=' + slide.imageText + '&w=400&h=50';
}
});
};
We should have something that will aggregate all of our slides. Letâs build that now:
var slideShowModelFactory = function (slides) {
return mobx.observable({
// observable
slides: _.map(slides, function (slide) {
return slideModelFactory(slide.text, slide.active);
}),
// computed
get activeSlide() {
return _.find(this.slides, {
active: true
});
}
});
};
The slideshow lives! This is more interesting because we have an observable slides array that will allow us to add and remove slides from the collection and have our UI update accordingly. Next, we add the activeSlide computed value that will keep itself current as needed.
Letâs render our slideshow. Weâre not ready for the HTML output yet so weâll just print to console.
var slideShowModel = slideShowModelFactory([
{
text: 'Heloo!',
active: true
}, {
text: 'Cool!'
}, {
text: 'MobX!'
}
]);
// this will output our data to the console
mobx.autorun(function () {
_.forEach(slideShowModel.slides, function(slide) {
console.log(slide.imageText + ' active: ' + slide.active);
});
});
// Console outputs:
// Heloo! active: true
// Cool! active: false
// MobX! active: false
Cool, we have a few slides and the autorun just printed out their current state. Letâs change a slide or two:
slideShowModel.slides[1].imageText = 'Super cool!';
// Console outputs:
// Heloo! active: true
// Super cool! active: false
// MobX! active: false
Looks like our autorun is working. If you change anything that autorun is watching, it will fire. Letâs change our output derivation from the console to HTML:
var $slideShowContainer = $('#slideShow');
mobx.autorun(function () {
var html = '<div class="mainImage"><img src="'
+ slideShowModel.activeSlide.imageMain
+ '"/></div>';
html += '<div id="slides">';
_.forEach(slideShowModel.slides, function (slide) {
html += '<div class="slide ' + (slide.active ? ' active' : '')
+ '" data-slide-id="' + slide.id + '">';
html += '<img src="' + slide.imageThumb + '"/>'
html += '</div>';
});
html += '</div>';
$slideShowContainer.html(html);
});
We now have the basics of this slideshow displaying, however, thereâs no interactivity yet. You canât click on a thumbnail and change the main image. But, you can change the image text and add slides using the console easily:
// add a new slide
slideShowModel.slides.push(slideModelFactory('TEST'));
// change an existing slide's text
slideShowModel.slides[1].imageText = 'Super cool!';
Letâs create our first and only action in order to set the selected slide. Weâll have to modify slideShowModelFactory by adding the following action:
// action
setActiveSlide: mobx.action('set active slide', function (slideId) {
// deactivate the current slide
this.activeSlide.active = false;
// set the next slide as active
_.find(this.slides, {id: slideId}).active = true;
})
Why use an action you ask? Great question! MobX actions are not required, as Iâve shown in my other examples on changing observable values.
Actions help you in a few ways. First, MobX actions are all run in transactions. What that means is that our autorun and other MobX reactions, will wait until the action has finished before firing. Think about that for a second. What would have happend if I tried to deactivate the active slide and activate the next one outside of a transaction? Our autorun would have fired twice. The first run would have been pretty awkward, as there would have been no active slide to display.
In addition to their transactional nature, MobX actions tend to make debugging simpler. The first optional parameter that I passed into my mobx.action is the string 'set active slide'. This string may be output with MobXâs debugging APIs.
So we have our action, letâs wire up its usage using jQuery:
$slideShowContainer.on('click', '.slide', function () {
slideShowModel.setActiveSlide($(this).data('slideId'));
});
Thatâs it. You may now click on the thumbnails and the active state propagates as youâd expect. Hereâs a working example of the slideshow:
See the Pen Simple MobX jQuery example by SitePoint (@SitePoint) on CodePen.
Hereâs a React example of the same slideshow.
Notice how I have not changed the model at all? As far as MobX is concerned, React is just another derivation of your data, like jQuery or the console.
Caveats to the jQuery slideshow example
Please note, I have not optimized the jQuery example in any way. Weâre clobbering the whole slideshow DOM on every change. By clobbering, I mean we are replacing all the HTML for the slideshow on every click. If you were to build a robust jQuery based slideshow, you would probably tweak the DOM after the initial render by setting and removing the active class and changing the src attribute of the mainImageâs <img>.
Want to learn more?
If I whet your appetite to learn more about MobX, check out some of the other useful resources below:
- MobX blogs, tutorials and videos
- Egghead.io course: Manage Complex State in React Apps with MobX
- Practical React with MobX
- Simple MobX Examples
- MobX API reference
If you have any questions, please hit me up in the comments below or come find me on the MobX gitter channel.
Frequently Asked Questions (FAQs) about Managing JavaScript Application State with MobX
How does MobX differ from other state management libraries?
MobX stands out from other state management libraries due to its simplicity and efficiency. Unlike Redux, which requires you to dispatch actions to change the state, MobX allows you to modify the state directly. It uses observables to track changes in the state and automatically updates the components that depend on these changes. This makes your code more readable and easier to maintain. Moreover, MobX is highly optimized for performance, ensuring that only the components that need to be re-rendered are updated.
How do I create an observable state in MobX?
In MobX, you can create an observable state by using the observable decorator. This decorator can be used on class properties to make them observable. Hereâs an example:import { observable } from 'mobx';class Store {
@observable count = 0;}
In this example, count is an observable property. Any changes to count will be tracked by MobX, and any components that depend on count will be automatically updated.
How do I modify the state in MobX?
In MobX, you can modify the state directly. However, itâs recommended to use actions for this purpose. Actions are functions that modify the state. They can be defined using the action decorator. Hereâs an example:import { observable, action } from 'mobx';class Store {
@observable count = 0;
@action
increment() {
this.count++;
}}
In this example, increment is an action that increases the count by 1. When this action is called, MobX will automatically update any components that depend on count.
How do I use computed values in MobX?
Computed values are values that are derived from the state. They are defined using the computed decorator. Hereâs an example:import { observable, computed } from 'mobx';class Store {
@observable count = 0;
@computed
get isEven() {
return this.count % 2 === 0;
}}
In this example, isEven is a computed value that returns true if the count is even, and false otherwise. Any changes to count will automatically update isEven.
How do I handle asynchronous actions in MobX?
MobX supports asynchronous actions out of the box. You can use async/await syntax in your actions. Hereâs an example:import { observable, action } from 'mobx';class Store {
@observable count = 0;
@action
async incrementAsync() {
await someAsyncOperation();
this.count++;
}}
In this example, incrementAsync is an asynchronous action that increases the count by 1 after an asynchronous operation.
How do I use MobX with React?
MobX can be easily integrated with React using the mobx-react package. This package provides a Provider component that allows you to pass your stores down the component tree, and an observer decorator that makes your components reactive.
How do I test MobX applications?
Testing MobX applications is straightforward. You can test your stores independently from your components, and vice versa. For testing stores, you can simply create an instance of your store, call its actions, and check its state. For testing components, you can use libraries like Enzyme or React Testing Library.
Can I use MobX with TypeScript?
Yes, MobX has full support for TypeScript. It provides type definitions out of the box, and its decorators work well with TypeScript decorators.
How do I debug MobX applications?
MobX provides several tools for debugging. The mobx-logger package logs all state changes and actions to the console. The MobX developer tools extension for Chrome provides a visual interface for inspecting your state and actions.
Can I use MobX with server-side rendering (SSR)?
Yes, MobX supports server-side rendering. You can use the useStaticRendering function from the mobx-react package to ensure that your components are rendered correctly on the server.