Reactive effects with automatic dependency management, caching, and leak-free finalization.
npm install resonant
resonant
currently supports UMD, CommonJS (node versions >= 10), and ESM build-targets
Commonjs:
const{resonant,effect}=require('resonant');
ESM:
import{resonant,effect}from'resonant';
Inspired by React'suseEffect
and Vue'swatchEffect
,resonant
is 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 theeffect
initializer). 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
.
To create an effect, you must first make the target object (the effect state) reactive with theresonant
function:
import{resonant}from'resonant';
constplainObject={
x:1,
y:1
};
constr=resonant(plainObject);
r
is 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 tracksr
as a reactive dependency. Any timer.x
orr.y
is 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.
resonant
uses 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.
To control an effect, each effect initializer returns uniquestop
,start
,andtoggle
handlers. These functions are used to pause, resume, or toggle the effect's active state.
Usestop
to pause an effect. The effect will not run during this period. Stopping an effect flushes its dependency cache, so subsequentstart
ortoggle
calls 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
Usestart
to transition the effect to an active state.start
is idempotent; if the effect is already active, invokingstart
willnotimmediately 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
Usetoggle
to toggle the effect's active state. Toggle invokes the appropriatestart
orstop
handler 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
Effects may be initialized lazily with thelazy
option. Passing this optional flag to theeffect
initializer will initialize the effect in an inactive state. The effect willnotrun immediately; either the effect'sstart
ortoggle
handlermust 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