What’s all this fuss about data-binding?
How do we keep our UI in sync with the data it uses? This is the fundamental problem of the frontend web. It’s the reason the javascript landscape is littered with “view layers”, MVC and MVVM frameworks, and fancy state containers.
For example, one very popular javascript library forces you to call a special function setState
when you want to update data that may affect UI; another very popular library meticulously wraps all of your application code in special $digest
loops so that afterwards it can explicitly re-render any UI that may have changed.
But what’s the point of all this? We face the simple challenge of turning javascript data into DOM — why must we come up with special ways of storing, updating, or accessing our data?
To understand the problem, let’s look at a function that renders a little VanillaJS component:
function render(person) {
var el = document.findElementById('hello');
el.innerHTML = "Hello, " + person.name;
}
It’s obvious which data this function depends on: person.name
. Yes, it takes a whole person as an argument, but the only value that affects its behavior is the name of the person. The person’s age, the person’s height, and their twitter handle do not affect the render at all.
Now, imagine we want to always keep the component up-to-date. Easy! Every time we update person.name
we call render(person)
afterwards. But this gets tedious — every new developer needs to know to do this, and maybe some of our old code updated person.name
without rendering.
Maybe we can just call render(person)
every few milliseconds? Meh, could work, but obviously wouldn’t be ideal for more expensive renders. And there would be a delay between data updates and UI changes.
One very clean approach is to override person
's name
getter and setter (assuming person
is an instance of an ES6 class):
class Person {
_name; get name() {
return this._name;
} set name(newName) {
this._name = newName;
render(this);
}
}
Cool. Now all of our old code and future code will work (as long as we don’t do anything too crazy with our person.name
property, like changing its descriptor).
But what if next week we update our render
function to this:
function render(person) {
var el = document.findElementById('hello');
el.innerHTML = "Hello, " + person.name +
". It looks like you're already " + person.age + " years old. Yikes. 😬";
}
Annoying! Now render(person)
depends on person.name
and person.age
. Now we have to do the same thing with the age
property of Person
:
class Person {
_name;
_age; get name() {
return this._name;
} set name(newName) {
this._name = newName;
render(this);
} get age() {
return this._age;
} set age(newAge) {
this._age = newAge;
render(this);
}
}
Great. But now if we have a renderFooter
function that also depends on person.name
, we’ll have to keep that in sync too:
function renderFooter(person) {
var el = document.findElementById('footer');
el.innerHTML = "This is the account of " + person.name;
}
Now we have two things to update in our name
setter:
set name(newName) {
this._name = newName;
render(this);
renderFooter(this);
}
This sort of callback whack-a-mole quickly becomes unsustainable. Not only is it hard to explicitly add a data dependency, but removing them when necessary becomes tedious as well.
What we need here is one of those “data-binding” systems, which will do two things:
- Listen for data changes.
- When the data changes, re-run code that depends on that data (e.g. rendering our components).
At PatientBank, we use MobX to do these things. It is by no means the only solution: we’ve tried AngularJS, React setState
, and Redux; and countless other libraries exist for this very purpose.
In our minds, MobX outshines other options because it avoids dictating how to store and update your state. We can use our own domain model classes like Person
just like we did above. It works well with our views (React), but you can easily use it to run any code when data changes.
It also requires almost no boilerplate code. You can write the entire app above using MobX in just a few lines:
class Person {
@observable name; // These are a super fancy ES7 decorators
@observable age; // Optional, but awesome!
}const person = new Person();
person.name = "Graham";
person.age = 22;function render(person) {
var el = document.findElementById('hello');
el.innerHTML = "Hello, " + person.name +
". It looks like you're already " + person.age + " years old. Yikes. 😬";
}function renderFooter(person) {
var el = document.findElementById('footer');
el.innerHTML = "This is the account of " + person.name;
}autorun(() => render(person));
autorun(() => renderFooter(person));
The only things we’ve added are @observable
decorators to the name
and age
fields of Person
. @observable
overrides getters and setters, tracking each time those properties are updated or accessed. Then, calling render
and renderFooter
in a MobX autorun
is enough to subscribe to those trackable properties and re-render the components whenever they change.
Done! No need to play whack-a-mole — MobX whacks every mole automatically.
We can easily add more @observable
fields to Person
(or to any other class) and add more render
functions that we want to run automatically. We can update our render
functions and they will always subscribe to the right data. We can even refactor our views to use a library like React, all without changing our underlying data structures.
MobX has allowed us at PatientBank to think very little about how we keep our UI in sync with our data. Instead, we can focus on other things, like gathering thousands of medical records for our patients 😎.
You can learn more about MobX in its docs, or by watching an EggHead tutorial by its creator.
If you love simplifying complex systems, let us know. We’re looking for driven, creative and resourceful people to join our team. Take a look at our current openings, or drop us a line at careers@patientbank.us.