@fluentkit/observable
v1.0.4
Published
A lightweight object proxy for reactivity with dependency tracking, watchers, effects and cached object getters.
Downloads
10
Maintainers
Readme
@fluentkit/observable
A lightweight 3KB (minified), 1KB gzipped, zero dependency* object proxy for reactivity with dependency tracking, watchers, effects and cached object getters.
Inspired by VueJs Reactivity.
Watchers and effects are batched, de-duped and called asynchronously using promises for performance.
* Requires the javascript Promise function, and the Reflect api. Internet Explorer is NOT supported.
Usage
To create a reactive object, import the observable function and provide your initial object as its only argument.
import {observable} from '@fluentkit/observable';
const reactiveObj = observable({
foo: 'bar',
bazzer: {
one: 'two',
three: ['four', 'five', 'six']
},
get computedValue() {
return this.bazzer.one + ',' + this.bazzer.three.join(',');
}
});
CDN Usage
You can use a prebuilt copy of the package from the sources below:
https://unpkg.com/@fluentkit/observable
https://cdn.jsdelivr.net/npm/@fluentkit/observable
In both case the observable function will be available on the global variable FluentKit
:
const obj = FluentKit.observable({});
API
$watch: (PropertyKey | PropertyKey[] | Function, callback?: Function): void
To watch for data changes you can use the watch function:
// when you provide just one argument the watcher is called on all changes:
reactiveObj.$watch((propertyName) => {
// here propertyName equals the (possibly nested) object key that was changed.
});
// or indicate string property to watch:
reactiveObj.$watch('foo', () => {
// reactiveObj.foo changed.
});
// and finally watch multiple properties with one callback:
reactiveObj.$watch(['foo', 'bazzer.one'], (propertyName) => {
// propertyName changed.
});
$watchSync: (PropertyKey | PropertyKey[] | Function, callback?: Function): void
Provides the same api as $watch
but runs the callback function immediately.
This method is used internally to clear cached computed values, it is exposed but most of your needs should be covered by $watch
.
$effect: (callback: () => {}): void
Effect when called first evaluates the supplied callback, tracking any dependencies accessed. Then when those dependencies change the callback is re-run, re-tracking the dependencies.
reactiveObject.$effect(() => {
console.log('effect called!', 'foo is:', reactiveObj.foo, 'bazzer is:', reactiveObj.bazzer);
});
// >> effect called .....
reactiveObject.bazzer.one = 'three'
// >> effect called .....
reactiveObj.newProperty = 'foobarbazzer'
// >> effect NOT called
$track: (callback: () => {}): string[]
Mainly used internally the $track
method returns the property keys accessed during the evaluation of its supplied callback.
const dependencies = reactivObj.$track(() => {
const foo = reactiveObj.foo;
const bazzer = reactiveObj.bazzer;
});
// dependencies = ['foo', 'bazzer'];
$nextTick: (callback?: () => {}): Promise
Allows you to run actions after any watchers and effects have been applied for the current observables modifications.
$nextTick
returns a promise, so you can provide a then
callback, or await $nextTick
in async functions.
reactiveObj.foo = 'zab';
// modifications here runs before any watchers/effects on `foo`
await reactiveObj.$nextTick();
// modifications here run AFTER watchers and effects for `foo`
$isSettled: boolean
Indicates if all watchers and effects have been run, and no new items have been passed to the queue.
$isObservable: boolean
Indicates an object is already an observable.
Computed values
Borrowed from Vue, "computed" values are just native object getters which can be defined upon creation:
const obj = observable({
foo: 'bar',
get computed() {
return this.foo.split('').reverse().join('');
}
});
Or added later using Object.defineProperty
:
Object.defineProperty(obj, 'computed', {
get () {
return this.foo.split('').reverse().join('');
}
});
Getters or "computed" properties are great for intensive operations. What's more when accessed their values are cached and returned without re-invoking until one of their dependencies change:
const obj = observable({
foo: 'bar',
bazzer: 'rezzab',
get computed() {
console.log('called');
return this.foo.split('').reverse().join('');
}
});
let computed = obj.computed; // === rab
// >> called
computed = obj.computed; // === rab
obj.bazzer = 'bazzer';
computed = obj.computed; // === rab
computed = obj.computed; // === rab
computed = obj.computed; // === rab
// >> NOT called
obj.foo = 'rab';
computed = obj.computed; // === bar
// >> called
computed = obj.computed; // === bar
computed = obj.computed; // === bar
computed = obj.computed; // === bar
// >> NOT called
Nested Observables
Observables can be nested and watched, to watch the whole child object:
obj.child = observable({ foo: 'bar' });
obj.$watch('child', () => {
// child, was reassigned, deleted, or its internal values changed.
});
obj.$watch('child.foo', () => {
// child.foo, was reassigned, deleted, or its value changed.
});