Skip to content

Instantly share code, notes, and snippets.

@jhoguet
Created December 13, 2016 04:35
Show Gist options
  • Save jhoguet/59a49cfb36084f3cab46cf739d83bd77 to your computer and use it in GitHub Desktop.
Save jhoguet/59a49cfb36084f3cab46cf739d83bd77 to your computer and use it in GitHub Desktop.
in trying to wrap my head around mobx, I found it easiest to see how it handles a few scenarios
import { observable, observe, transaction, computed, reaction } from 'mobx';
import expect, { createSpy } from 'expect';
describe('learning mobx', () => {
it('some mobx basics', () => {
const computedSpy = createSpy();
const o1 = observable(1);
const o2 = observable(10)
const o3 = observable(() => {
computedSpy();
return o1.get() + o2.get();
});
// computed is lazy, no computation yet
expect(computedSpy.calls.length).toBe(0);
expect(o1.get()).toBe(1);
expect(o2.get()).toBe(10);
expect(o3.get()).toBe(11);
expect(computedSpy.calls.length).toBe(1);
computedSpy.reset();
o1.set(2);
expect(computedSpy.calls.length).toBe(0);
expect(o1.get()).toBe(2);
expect(o2.get()).toBe(10);
expect(o3.get()).toBe(12);
expect(computedSpy.calls.length).toBe(1);
computedSpy.reset();
o2.set(20);
expect(computedSpy.calls.length).toBe(0);
expect(o1.get()).toBe(2);
expect(o2.get()).toBe(20);
expect(o3.get()).toBe(22);
expect(computedSpy.calls.length).toBe(1);
computedSpy.reset();
o1.set(3);
o2.set(30);
expect(computedSpy.calls.length).toBe(0);
expect(o1.get()).toBe(3);
expect(o2.get()).toBe(30);
expect(o3.get()).toBe(33);
// only exectued computed once, because it is lazy
expect(computedSpy.calls.length).toBe(1);
computedSpy.reset();
// now add observer which changes everything
const o1Spy = createSpy();
observe(o1, o1Spy);
expect(o1Spy.calls.length).toBe(0);// hasn't been invoked yet... odd
const o2Spy = createSpy();
// we can opt-in to firing immediately
observe(o2, o2Spy, true);
expect(o2Spy.calls.length).toBe(1);
o2Spy.reset();
const o3Spy = createSpy();
observe(o3, o3Spy);
expect(o3Spy.calls.length).toBe(0);
o1.set(4);
expect(o1Spy.calls.length).toBe(1);
expect(o2Spy.calls.length).toBe(0);
// notice computed isn't lazy anymore (makes sense since someone is observing it)
expect(o3Spy.calls.length).toBe(1);
o1Spy.reset();
o3Spy.reset();
// what if I change more than one thing
o1.set(5);
o2.set(50);
expect(o1Spy.calls.length).toBe(1);
expect(o2Spy.calls.length).toBe(1);
expect(o3Spy.calls.length).toBe(2);
o1Spy.reset();
o2Spy.reset();
o3Spy.reset();
// what if I change more than one thing in a transaction
transaction(() => {
o1.set(6);
o2.set(60);
});
expect(o1Spy.calls.length).toBe(1);
expect(o2Spy.calls.length).toBe(1);
expect(o3Spy.calls.length).toBe(1); // only one observation this time
o1Spy.reset();
o2Spy.reset();
o3Spy.reset();
// what if I change the same thing more than once...
o1.set(7);
o1.set(8);
expect(o1Spy.calls.length).toBe(2);
expect(o2Spy.calls.length).toBe(0);
expect(o3Spy.calls.length).toBe(2);
o1Spy.reset();
o3Spy.reset();
// what if I set a value to the same value
o1.set(o1.get());
o2.set(o2.get());
expect(o1Spy.calls.length).toBe(0);
expect(o2Spy.calls.length).toBe(0);
expect(o3Spy.calls.length).toBe(0);
o1Spy.reset();
o3Spy.reset();
});
describe('depper into computed', () => {
it('is lazy', () => {
const o = observable(1);
const cSpy = createSpy().andCall(() => o.get() + 1);
const c = observable(() => cSpy());
expect(cSpy.calls.length).toBe(0);
});
it('will not cache even if dependencies have not changed', () => {
const o = observable(1);
const cSpy = createSpy();
const c = computed(() => {
cSpy();
return o.get() + 1;
});
c.get(); //
c.get();
c.get();
// ideally this would be 1 since the dependency o, has not changed
expect(cSpy.calls.length).toBe(3);
});
it('not lazy and caches when observed (inconsistency)', () => {
// which is kind of weird, because
const o = observable(1);
const cSpy = createSpy().andCall(() => o.get() + 1);
const c = observable(() => cSpy());
const observeCSpy = createSpy();
observe(c, observeCSpy);
expect(observeCSpy.calls.length).toBe(0);
// even though observe didn't emit, it still computed
expect(cSpy.calls.length).toBe(1);
c.get();
c.get();
c.get();
// and now it doesn't re-compute every time
expect(cSpy.calls.length).toBe(1);
// this is a very odd side effect - it seems like you should get this same behavior either way
});
it('should observe changes to its dependencies', () => {
const o = observable(1);
const cSpy = createSpy().andCall(() => o.get() + 1);
const c = observable(() => cSpy());
expect(cSpy.calls.length).toBe(0); // in lazy mode
c.get();
expect(cSpy.calls.length).toBe(1);
cSpy.reset();
o.set(2);
expect(cSpy.calls.length).toBe(0); // in lazy mode
c.get();
c.get();
c.get();
expect(cSpy.calls.length).toBe(3); // no caching
cSpy.reset();
// then we start observing and the behavior flips
const cObserver = createSpy();
observe(c, cObserver);
expect(cSpy.calls.length).toBe(1); // computed as soon as we observed
cSpy.reset();
cObserver.reset();
o.set(o.get() + 1);
expect(cSpy.calls.length).toBe(1); // now it proactively recomputes
expect(cObserver.calls.length).toBe(1);
cSpy.reset();
cObserver.reset();
c.get();
c.get();
c.get();
expect(cSpy.calls.length).toBe(0); // now it is caching
});
it('should observe changes to dependencies as they evolve', () => {
const o1 = observable(1);
const o2 = observable(10);
const c = computed(() => {
if (o1.get() === 1){
return 1;
}
else {
return o1.get() + o2.get();
}
});
const cSpy = createSpy();
observe(c, cSpy);
cSpy.reset();
// first time through, it won't be tracking o2, which is
// acceptible since that path isn't reachable til o1 changes
// but .... if we change o1, will it START tracking o2
o1.set(2);
expect(cSpy.calls.length).toBe(1);
cSpy.reset();
// now is it tracking o2?
o2.set(20);
expect(cSpy.calls.length).toBe(1);
// it is... great
});
it('can async', () => {
function fromPromise(p){
const o = observable(undefined);
p.then(val => o.set(val));
return o
}
const spy = createSpy();
const p = Promise.resolve(1);
const o = fromPromise(p);
observe(o, spy, true);
expect(spy.calls.length).toBe(1)
expect(typeof o.get()).toBe('undefined');
spy.reset();
return p.then(() => {
expect(spy.calls.length).toBe(1);
expect(o.get()).toBe(1);
})
});
it('can computedAsync', () => {
function asyncComputed(mapToDependencies, dependenciesToPromise){
const innerObservable = observable();
reaction(mapToDependencies, dependencies => {
innerObservable.set(undefined);
dependenciesToPromise(dependencies)
.then(val => innerObservable.set(val));
}, true);
return computed(() => {
return innerObservable.get();
});
}
const o = observable('Jon');
const oToPromise = name => Promise.resolve(`Hello ${name}`);
const c = asyncComputed(() => o.get(), oToPromise);
const spy = createSpy();
observe(c, spy, true);
expect(spy.calls.length).toBe(1)
expect(typeof c.get()).toBe('undefined');
spy.reset();
return Promise.resolve()
.then(() => {
expect(spy.calls.length).toBe(1)
expect(c.get()).toBe('Hello Jon');
spy.reset();
})
.then(() => {
o.set('Angela');
expect(c.get()).toBe(undefined);
expect(spy.calls.length).toBe(1)
spy.reset();
})
.then(() => {
expect(spy.calls.length).toBe(1)
expect(c.get()).toBe('Hello Angela');
spy.reset();
});
})
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment