MOBservable
Installation: npm install mobservable --save
MOBservable is light-weight stand-alone library to create reactive primitives, functions, arrays and objects.
Its goal is to make developers happy and productive, by removing boilerplate work such as invalidating derived data or managing event listeners.
It makes sure data changes are automatically, atomically and synchronously propagated through your app without being obtrusive.
MOBservable runs in any ES5 environment but features also some React addons.
It is higly efficient and shines when managing large amounts of complex, cyclic, nested or computed data.
Examples
The source of all demo's can also be found in the example folder.
Example: Observable values and functions
The core of MOBservable
consists of observable values, functions that automatically recompute when an observed value changes,
and the possibility to listen to changing values and updated computations.
var mobservable = require('mobservable');
var nrOfCatz = mobservable(3);
var nrOfDogs = mobservable(8);
var nrOfAnimals = mobservable(function() {
return nrOfCatz() * nrOfDogs();
});
nrOfAnimals.observe(function(amount) {
console.log("Total: " + amount);
}, true);
nrOfCatz(34);
Example: Observable objects & properties
By using .props
, it is possible to create observable values and functions that can be assigned or read as normal properties.
var mobservable = require('mobservable');
var Person = function(firstName, lastName) {
mobservable.props(this, {
firstName: firstName,
lastName: lastName,
fullName: function() {
return this.firsName + " " + this.lastName;
}
});
}
var jane = new Person("Jane","Dôh");
console.log(jane.fullName);
mobsevable.observeProperty(jane, "fullName", console.log);
jane.lastName = "Do";
Example: Observable arrays
mobservable
provides an observable array implementation (as ES7 polyfill), which is fully ES5 compliant,
but which will notify dependent computations upon each change.
import mobservable = require('mobservable');
var someNumbers = mobservable.value([1,2,3]);
var sum = mobservable.value(function() {
for(var s = 0, i = 0; i < someNumbers.length; i++)
s += someNumbers[i];
return s;
});
sum.observe(console.log);
someNumbers.push(4);
someNumbers[2] = 0;
someNumbers[someNumbers.length] = 5;
Example: TypeScript classes and annotations
For typescript users, mobservable
ships with module typings and an @observable
annotation with which class members can be marked as observable.
import mobservable = require('mobservable');
var observable = mobservable.observable;
class Order {
@observable orderLines: OrderLine[] = [];
@observable total() {
return this.orderLines.reduce((sum, orderLine) => sum + orderLine.total, 0)
}
}
class OrderLine {
@observable price:number = 0;
@observable amount:number = 1;
constructor(price) {
this.price = price;
}
@observable total() {
return "Total: " + this.price * this.amount;
}
}
var order1 = new Order();
order1.total.observe(console.log);
order1.orderLines.push(new OrderLine(7));
order1.orderLines.push(new OrderLine(12));
order1.orderLines[0].amount = 3;
Example: ObservingComponent for react components
MOBservable ships with a mixin and class decorator that can be used to subscribe React components to observables automatically.
The full JSX example can be found in this fjsiddle
var store = {};
mobservable.props(store, {
timer: 0
});
function resetTimer() {
store.timer = 0;
}
setInterval(function() {
store.timer += 1;
}, 1000);
var TimerView = mobservable.ObservingComponent(React.createClass({
render: function() {
return (<span>Seconds passed: {this.props.store.timer}</span>);
}
}));
var TimerApp = React.createClass({
render: function() {
var now = new Date();
return (<div>
<div>Started rendering at: {now.toString()}</div>
<TimerView {...this.props} />
<br/><button onClick={resetTimer}>Reset timer</button>
</div>);
}
});
React.render(<TimerApp store={store} />, document.body);
Design principles
Principles
MOBservable is designed with the following principles in mind.
- The Model, View (and Contorller) of an app should be separated.
Views should be loosely coupled to the UI, so that UI refactorings do not require changes of the data model.
It should be possible to describe views on the data model as naturally as possible, as-if data does not change over time.
- Derived data should be re-calculated automatically and efficiently.
It is the responsibility of MOBservable to prevent that views ever become stale.
- MOBservable is unobtrusive and doesnt place any constraints on how you build or work with data structures.
Inheritance, classes, cyclic data structures, or instance methods...? The library does not pose any restrictions on your data.
- Data should be mutable as this is close to the natural mental model of most kinds of data.
(despite some nice properties of immutable data, mutable data is easier to inspect, read, grok and especially more natural to program against.
markRead(email) { email.isRead = true; }
is more convenient to write than markRead(email) { return { ...email, isRead : true }; }
or markRead(email) { model.set('email', 'isRead', true); }
.
Especially when email is somewhere deep in your model tree) - Subscriptions should be a breeze to manage, and managed automatically wherever possible.
- MOBservable is only about the model data, not about querying, back-end communication etc (although observers are really useful there as well).
Behavior
Observable values, arrays and functions created by mobservable
possess the following characteristics:
- synchronous. Updates are processed synchronously, that is, the pseudo expressions
a = 3; b -> a * 2; a = 4; print(b);
will always print 8
; b
will never yield a stale value. - atomic. Computed values will postpone updates until all inputs are settled, to make sure no intermediate values are visible. That is, the expression
a = 3; b -> a * 2; c -> a * b; a = 4; print(c)
will always print 32
and no intermediate values like 24
. - real time dependency detection. Computed values only depend on values actually used in the last computation, for example, given:
a -> b > 5 ? c : b
the variable c
will only cause a re-evaluation of a
if b > 5
. - lazy. Computed values will only be evaluated if they are actually being observed. So make sure computed functions are pure and side effect free; the library might not evaluate expressions as often as you thought it would.
- cycle detection. Cycles in computations, like in
a -> 2 * b; b -> 2 * a;
will be deteced. - error handling. Exceptions that are raised during computations are propagated to consumers.
API Documentation
Typescript typings
Creating observables
mobservable
Shorthand for mobservable.value
mobservable.value
mobservable.value<T>(value? : T[], scope? : Object) : IObservableArray<T>
mobservable.value<T>(value? : T|()=>T, scope? : Object) : IObservableValue<T>
Function that creates an observable given a value
.
Depending on the type of the function, this function invokes mobservable.array
, mobservable.computed
or mobservable.primitive
.
See the examples above for usage patterns. The scope
is only meaningful if a function is passed into this method.
mobservable.primitive
mobservable.primitive<T>(value? : T) : IObservableValue<T>
Creates a new observable, initialzed with the given value
that can change over time.
The returned observable is a function, that without arguments acts as getter, and with arguments as setter.
Furthermore its value can be observed using the .observe
method, see IObservableValue.observe
.
Example:
var vat = mobservable.primitive(3);
console.log(vat()); // prints '3'
vat.observe(console.log); // register an observer
vat(4); // updates value, also notifies all observers, thus prints '4'
mobservable.reference
mobservable.reference<T>(value? : T) : IObservableValue<T>
Synonym for mobservable.primitive
, since the equality of primitives is determined in the same way as references, namely by strict equality.
(from version 0.6, see mobservable.struct
if values need to be compared structuraly by using deep equality).
mobservable.computed
mobservable.computed<T>(expr : () => T, scope?) : IObservableValue<T>
computed
turns a function into an observable value.
The provided expr
should not have any arguments, but instead really on other observables that are in scope to determine its value.
The latest value returned by expr
determines the value of the observable. When one of the observables used in expr
changes, computed
will make sure that the function gets re-evaluated, and all updates are propogated to the children.
var amount = mobservable(3);
var price = mobservable(2);
var total = mobservable.computed(function() {
return amount() * price();
});
console.log(total());
total.observe(console.log);
amount(4);
amount(4);
The optional scope
parameter defines this
context during the evaluation of expr
.
computed
will try to reduce the amount of re-evaluates of expr
as much as possible. For that reason the function should be pure, that is:
- The result of
expr
should only be defined in terms of other observables, and not depend on any other state. - Your code shouldn't rely on any side-effects, triggered by
expr
; expr
should be side-effect free. - The result of
expr
should always be the same if none of the observed observables did change.
It is not allowed for expr
to have an (implicit) dependency on its own value.
It is allowed to throw exceptions in an observed function. The thrown exceptions might only be detected late.
The exception will be rethrown if somebody inspects the current value, and will be passed as first callback argument
to all the listeners.
mobservable.expr
mobservable.expr<T>(expr : ()=>T, scope?) : T
This function is simply sugar for mobservable.computed(expr, scope)();
.
expr
can be used to split up and improve the performance of expensive computations,
as described in this section.
mobservable.sideEffect
mobservable.sideEffect(func:() => void, scope?): ()=>void
Use this function if you have a function which should produce side effects, even if it is not observed itself.
This is useful for logging, storage backend interaction etc.
Use it whenever you need to transfer observable data to things that don't know how to observe.
sideEffect
returns a function that can be used to prevent the sideEffect from being triggered in the future.
var x = mobservable(3);
var x2 = mobservable(function() {
return x() * 2;
});
mobservable.sideEffect(function() {
storeInDatabase(x2());
console.log(x2());
});
x(7);
mobservable.array
mobservable.array<T>(values? : T[]) : IObservableArray<T>
Note: ES5 environments only
Constructs an array like, observable structure. An observable array is a thin abstraction over native arrays and adds observable properties.
The most notable difference between built-in arrays is that these arrays cannot be sparse, that is,
values assigned to an index larger than length
are considered out-of-bounds and not oberved
(nor any other property that is assigned to a non-numeric pr negative index).
Furthermore, Array.isArray(observableArray)
and typeof observableArray === "array"
will yield false
for observable arrays,
but observableArray instanceof Array
will return true
.
var numbers = mobservable.array([1,2,3]);
var sum = mobservable.value(function() {
return numbers.reduce(function(a, b) { return a + b }, 0);
});
sum.observe(function(s) { console.log(s); });
numbers[3] = 4;
numbers.push(5,6);
numbers.unshift(10);
Observable arrays implement all the ES5 array methods. Besides those, the following methods are available as well:
observe(listener:(changeData:IArrayChange<T>|IArraySplice<T>)=>void, fireImmediately?:boolean):Lambda
Listen to changes in this array. The callback will receive arguments that express an array splice or array change, conform the ES7 proposalclear(): T[]
Remove all current entries from the arrayreplace(newItems:T[])
Replaces all existing entries in the array with new ones.values(): T[]
Returns a shallow, non-observable clone of the array, similar to .slice
clone(): IObservableArray<T>
Create a new observable array containing the same valuesfind(predicate:(item:T,index:number,array:IObservableArray<T>)=>boolean,thisArg?,fromIndex?:number):T
Find implementation, basically the same as the ES7 Array.find proposal, but with added fromIndex
parameter.remove(value:T):boolean
Remove a single item by value from the array. Returns true if the item was found and removed.
mobservable.props
props(target:Object, name:string, initialValue: any):Object;
props(target:Object, props:Object):Object;
props(target:Object):Object;
Note: ES5 environments only
Creates observable properties on the given target
object. This function uses mobservable.value
internally to create observables.
Creating properties has as advantage that they are more convenient to use. See also value versus props.
The original target
, with the added properties, is returned by this function. Functions used to created computed observables will automatically
be bound to the correct this
.
var order = {};
mobservable.props(order, {
amount: 3,
price: 5,
total: function() {
return this.amount * this.price;
}
});
order.amount = 4;
console.log(order.total);
Note that observables created by mobservable.props
do not expose an .observe
method,
to observe properties, see mobservable.observeProperty
Other forms in which this function can be called:
mobservable.props(order, "price", 3);
var order = mobservable.props({ price: 3});
mobservable.observable annotation
Note: ES5, TypeScript 1.5+ environments only
Typescript 1.5 introduces annotations. The mobservable.observable
annotation can be used to mark class properties and functions as observable.
This annotations basically wraps mobservable.props
. Example:
var observable = require('mobservable').observable;
class Order {
@observable price:number = 3;
@observable amount:number = 2;
@observable orders = [];
@observable total() {
return this.amount * this.price * (1 + orders.length);
}
}
Observing changes
mobservable.observeProperty
mobservable.observeProperty(object : Object, key : string, listener : Function, invokeImmediately : boolean = false) : Function
Observes the observable property key
of object
. This is useful if you want to observe properties created using the observable
annotation or the props
method,
since for those properties their own observe
method is not publicly available.
function OrderLine(price) {
mobservable.props(this, {
price: price,
amount: 2,
total: function() {
return this.price * this.amount;
}
});
}
var orderLine = new OrderLine(5);
mobservable.observeProperty(order, 'total', console.log, true);
mobservable.watch
mobservable.watch<T>(func: () => T, onInvalidate : Function) : [T, Function];
watch
is quite similar to mobservable.computed
, but instead of re-evaluating func
when one of its dependencies has changed, the onInvalidate
function is triggered.
So func
will be evaluated only once, and as soon as its value has become stale, the onInvalidate
callback is triggered.
watch
returns a tuple consisting of the initial return value of func
and an unsubscriber to be able to abort the watch.
The onInvalidate
function will be called only once, after that, the watch has finished.
watch
is useful in functions where you want to have a function that responds to change,
but where the function is actually invoked as side effect or as part of a bigger change flow or where unnecessary recalculations of func
or either pointless or expensive,
for example in the render
method of a React component.
mobservable.batch
mobservable.batch<T>(workerFunction : ()=>T):T
Batch postpones the updates of computed properties until the (synchronous) workerFunction
has completed.
This is useful if you want to apply a bunch of different updates throughout your model before needing the updated computed values,
for example while refreshing a data from the database. In practice, you wil probably never need .batch
, since observables usually update wickedly fast.
var amount = mobservable(3);
var price = mobservable(2.5);
var total = mobservable(function() {
return amount() * price();
});
total.observe(console.log);
amount(2);
price(3);
mobservable.batch(function() {
amount(3);
price(4);
});
Utilities
mobservable.toPlainValue
mobservable.toPlainValue<T>(any:T):T;
Converts a (possibly) observable value into a non-observablue value.
For non-primitive values, this function will always return a shallow copy.
mobservable.ObserverMixin
The observer mixin can be used in React components.
This mixin basically turns the .render
function of the component into an observable function, and makes sure that the component itself becomes an observer of that function,
so that the component is re-rendered each time an observable has changed.
This mixin also prevents re-renderings when the props of the component have only shallowly changed
(Similar to React PureRender mixin, except that state changes are still always processed).
This allows for React apps that perform well in apps with large amount of complex data, while avoiding the need to manage a lot of subscriptions.
See the above example or the JSFiddle demo: MOBservable + React
For an extensive explanation, read combing React with MOBservable
mobservable.ObservingComponent
mobservable.ObservingComponent(clazz:ReactComponentClass):ReactComponentClass
If you want to create a React component based on ES6 where mixins are not supported,
you can use the ObservingComponent
function to wrap around your React createClass
call (instead of using the mixin ObserverMixin
):
var myComponent = mobservable.ObservingComponent(React.createClass({
});
mobservable.debugLevel
Numeric property, setting this to value to '1' or higher will cause additional debug information to be printed.
mobservable.SimpleEventEmitter
Utility class for managing an event. Its instance methods are:
new mobservable.SimpleEventEmitter()
. Creates a new SimpleEventEmitter
emit(...data : any[])
. Invokes all registered listeners with the given argumentson(listener:(...data : any[]) => void) : () => void
. Registers a new callback that will be invoked on each emit
. Returns a method that can be used to unsubscribe the listener.once(listener:(...data : any[]) => void) : () => void
. Similar to .on
, but automatically removes the listener after one invocation.
Advanced Tips & Tricks
How to create lazy values?
All computed values are lazy and only evaluated upon first observation (or when their value is explicitly getted)
Use local variables in computations
Each time an observable value is read, there is a small performance overhead to keep the dependency tree of computations up to date.
Although this might not be noticable in practice, if you want to squeeze the last bit of performance out of the library;
use local variables as much as possible to reduce the amount of observable reads.
This also holds for array entries and object properties created using mobservable.props
.
var firstName = mobservable('John');
var lastName = mobservable('Do');
var fullName = mobservable(function() {
if (firstName())
return lastName() + ", " + firstName();
return lastName();
}
var fullName = mobservable(function() {
var first = firstName(), last = lastName();
if (first)
return last+ ", " + first;
return last;
}
Use nested observables in expensive computations
It is perfectly fine to create computed observables inside computed observables.
This is a useful pattern if you have an expensive computation that depends on a condition check that is fired often, but not changed often.
For example when your computation contains a cheap treshold check, or when your UI renderig depends on the some selection of the user.
For example:
var person;
var total = mobservable(function() {
if (person.age === 42)
doSomeExpensiveComputation();
else
doSomeOtherExpensiveComputation();
});
In the example above, every single time the person's age
changes, total
is computed by invoking some expensive computations.
However, if the expression page.age === 42
was put in a separate observable,
computing the total
itself could be avoided in many cases because a recomputation would only occur if the value of the complete expression changes.
Yet, you might not want to create separate stand-alone observables for these expressions,
because you don't have a nice place to put them or because it would make the readability of the code worse.
In such cases you can also create an inline observable.
In the following example, the total is only recalculated if the age changes to, or from, 42. Which means that for most other ages,
recomputing the expensive computations can be avoided.
var person;
var total = mobservable(function() {
var ageEquals42 = mobservable(function() { return person.age === 42 })();
if (ageEquals42)
doSomeExpensiveComputation();
else
doSomeOtherExpensiveComputation();
});
Note that the dangling ()
after the expression is meant to invoke the getter of the just created observable to obtain its value.
For convenience the same statement can also be rewritten using the expr function:
var ageEquals42 = mobservable.expr(function() { return person.age === 42 });
Use native array methods
For performance, use built-in array methods as much as possible;
a classic array for loop is registered as multiple reads, while a function call is registered as a single read.
Alternatively, slicing the array before using it will also result in a single read.
var numbers = mobservable([1,2,3]);
var sum1 = mobservable(function() {
var s = 0;
for(var i = 0; i < numbers.length; i++)
s += numbers[i];
return s;
});
var sum2 = mobservable(function() {
var s = 0, localNumbers = numbers.slice();
for(var i = 0; i < localNumbers.length; i++)
s += localNumbers[i];
return s;
});
var sum2 = mobservable(function() {
return numbers.reduce(function(a, b) {
return a + b;
}, 0);
});
.value
versus .props
The difference between obj.amount = mobservable.value(3)
and mobservable.props(obj, { value: 3 })
to create observable values inside an object might seem to be a matter of taste.
Here is a small comparison list between the two approaches.
.value
- ES3 compliant
- explicit getter/setter functions:
obj.amount(2)
- easy to make mistakes in assignments; e.g.
obj.amount = 3
instead of obj.amount(3)
, or 7 * obj.amount
instead of 7 * obj.amount()
- easy to manually observe:
obj.amount.observe(listener)
.props
- Requires ES5
- object properties with implicit getter/setter:
obj.amount = 2
- more natural to write / read values, syntactically you won't notice they are observable
- harder to manually observe:
mobservable.observeProperty(obj,'amount',listener)
.reference
versus .array
Do not confuse mobservable.reference([])
/ mobservable.primitive([])
with mobservable([])
/ mobservable.array([])
,
the first two create a observable reference to an array, but does not observe its contents.
The later two observe the content of the array you passed into it, which is probably what you inteded.