Skip to content

A lightweight < 3KB (minified), 1KB gzipped, zero dependency object proxy for reactivity with dependency tracking, watchers, effects and cached object getters.

License

Notifications You must be signed in to change notification settings

fluentkit/observable

Repository files navigation

@fluentkit/observable

NPM Downloads Bundlephobia Issues License NPM Downloads jsDelivr hits (npm) Unpkg

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.
});

About

A lightweight < 3KB (minified), 1KB gzipped, zero dependency object proxy for reactivity with dependency tracking, watchers, effects and cached object getters.

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Packages

No packages published