Skip to content

Reactive effects with automatic dependency management, caching, and leak-free finalization.

License

Notifications You must be signed in to change notification settings

exbotanical/resonant

Repository files navigation

resonant

Coverage Status Continuous Deployment Continuous Integration npm version License: MIT

Reactive effects with automatic dependency management, caching, and leak-free finalization.

Table of Contents

Installation

npm install resonant

Supported Environments

resonantcurrently supports UMD, CommonJS (node versions >= 10), and ESM build-targets

Commonjs:

const{resonant,effect}=require('resonant');

ESM:

import{resonant,effect}from'resonant';

Documentation

Inspired by React'suseEffectand Vue'swatchEffect,resonantis a compact utility library that mitigates the overhead of managing observable data, such as dependency tracking; caching and cache invalidation; and object dereferencing and finalization.

Inresonant,an effect is a computation that is automatically invoked any time its reactive state changes. An effect's reactive state is any state that is accessed inside the effect body (specifically, the function passed to theeffectinitializer). A deterministic heuristic follows that any data access that triggers getters will be visible to and therefore tracked by the effect.

This reactive state 'resonates', henceresonant.

Creating an Effect

To create an effect, you must first make the target object (the effect state) reactive with theresonantfunction:

import{resonant}from'resonant';

constplainObject={
x:1,
y:1
};

constr=resonant(plainObject);

ris now equipped with deep reactivity. All getters / setters will trigger any effects that happen to be observing the data.

Let's create an effect:

import{resonant,effect}from'resonant';

constplainObject={
x:1,
y:1
};

constr=resonant(plainObject);

letcount=0;

effect(()=>{
count+=r.x+r.y;
});

The effect will be invoked immediately. Next, the effect is cached and tracksras a reactive dependency. Any timer.xorr.yis mutated, the effect will run.

This paradigm works with branching and nested conditionals; if the effect encounters new properties by way of conditional logic, it tracks them as dependencies.

constr=resonant({
x:{
y:{
z:1
}
}
});

effect(()=>{
if(r.x.y.k){
if(r.x.y.z>1){
computation();
}
}
});

// the outer condition evaluates to `false`
// the effect doesn't need to know about `r.x.y.z` yet - we'll track it only when necessary

r.x.y.k=1;
// now, the outer condition evaluates to `true`
// the effect will see the second condition and begin tracking `r.x.y.z`

Effect dependencies are tracked lazily; the effect only ever cares about resonant data that it can see.

resonantuses weak references; deleted properties to which there are no references will be finalized so they may be garbage collected, as will all of that property's dependencies and effects.

Starting and Stopping an Effect

To control an effect, each effect initializer returns uniquestop,start,andtogglehandlers. These functions are used to pause, resume, or toggle the effect's active state.

Usestopto pause an effect. The effect will not run during this period. Stopping an effect flushes its dependency cache, so subsequentstartortogglecalls are akin to creating the effect anew.

import{resonant,effect}from'resonant';

constr=resonant({x:1});

letc=0;
const{stop}=effect(()=>{
c+=r.x;
});

// initial run - `c` == 1

r.x++;

// trigger - `c` == 3

stop();

r.x++;

// `c` == 3

Usestartto transition the effect to an active state.startis idempotent; if the effect is already active, invokingstartwillnotimmediately trigger the effect. Otherwise,start- like instantiating a new effect - will run the effect immediately.

import{resonant,effect}from'resonant';

constr=resonant({x:1});
letc=0;

const{stop,start}=effect(()=>{
c+=r.x;
});
// initial run - r.x == 1, c == 1

r.x++;
// r.x == 2, c == 3

stop();

r.x++;
// r.x == 3, c == 3

start();
// initial run - r.x == 3, c == 6

r.x++;
// r.x == 4, c == 7

Usetoggleto toggle the effect's active state. Toggle invokes the appropriatestartorstophandler and returns a boolean indicating whether the effect's state is active.

import{resonant,effect}from'resonant';

constr=resonant({x:1});
letc=0;
letisActive=true;

const{toggle}=effect(()=>{
c+=r.x;
});
// initial run - r.x == 1, c == 1

r.x++;
// r.x == 2, c == 3

isActive=toggle();
// isActive == false

r.x++;
// r.x == 3, c == 3

isActive=toggle();
// isActive == true
// initial run - r.x == 3, c == 6

r.x++;
// r.x == 4, c == 7

Deferred Effects

Effects may be initialized lazily with thelazyoption. Passing this optional flag to theeffectinitializer will initialize the effect in an inactive state. The effect willnotrun immediately; either the effect'sstartortogglehandlermust be invoked before the effect can trigger.

import{resonant,effect}from'resonant';

constr=resonant({x:1});
letc=0;

const{start}=effect(()=>{
c+=r.x;
},{lazy:true});
// no initial run - r.x == 1, c == 0

r.x++;
// r.x == 2, c == 0

start();

r.x++;
// r.x == 3, c == 3

Full documentation and type signatures can be foundhere