MobX-style reactivity turns a plain object into an observable graph: reading one of its properties inside a reaction subscribes that reaction to the property, and writing a new value re-runs every reaction that read it — automatically, with no manual subscriptions and no dependency arrays. You never list what a function depends on; the library learns it by watching which properties the function actually touches while it runs. Build a tiny version of that engine with three pieces: observable, autorun, and computed.
observable(target) wraps a plain object in a Proxy so every read is tracked and every write notifies the readers. autorun(fn) runs fn right away, remembers which observable properties it read, and re-runs it whenever any of those change; it returns a dispose() that stops the tracking. computed(fn) is a memoized derived value: it recomputes only when a dependency changed, and it is itself a dependency for any reaction that reads it.
const miniMobx: {
// Wrap a plain object; reads are tracked, writes trigger reactions.
observable<T extends object>(target: T): T;
// Run fn now and again on every change to what it read.
// Returns dispose() to stop re-running it.
autorun(fn: () => void): () => void;
// A memoized derived value; get() reads it (and tracks it).
computed<T>(fn: () => T): { get(): T };
};
const { observable, autorun } = miniMobx;
const state = observable({ first: 'Ada', last: 'Lovelace' });
const log = [];
// Runs once now (reads first + last), then again on any change to them.
autorun(() => log.push(state.first + ' ' + state.last));
// log: ['Ada Lovelace']
state.last = 'L.'; // re-runs → log: ['Ada Lovelace', 'Ada L.']
state.last = 'L.'; // same value → no re-run
state.middle = 'K.'; // never read → no re-run
const { observable, computed } = miniMobx;
const cart = observable({ price: 10, qty: 2 });
let runs = 0;
const total = computed(() => {
runs++;
return cart.price * cart.qty;
});
total.get(); // 20 (computed once, runs === 1)
total.get(); // 20 (cached, runs === 1)
cart.qty = 3;
total.get(); // 30 (recomputed, runs === 2)
Object.is; writing the identical value triggers nothing.if branch it no longer takes) stops triggering it.computed is lazy and memoized — it recomputes only when one of the properties it read changed; reading it otherwise returns the cached value, and two readers share a single computation.Map/Set, batching many writes into one flush, or the in/delete operators. Observe own properties one level deep and re-run synchronously on each write.