1.0.3 • Published 2 years ago

jay-reactive v1.0.3

Weekly downloads
-
License
-
Repository
-
Last release
2 years ago

Reactive Module

The Reactive module is a minimal reactive core implementation that handles storing data, reacting to data change and detecting if data has actually changed.

Reactive will strive to run reactions as a batch and will do so sync when using batchReactions or async if batchReactions was not used. When there are pending reactions to be run async, toBeClean can be used to wait for the reactions to run using await reactive.toBeClean().

It is intended to be an internal core implementation for state management and not a user facing API.

The package has 3 modules

Reactive Class

The Reactive class is a simple reactive core, at which reactions are dependent on state. When a state is updated, any of the dependent reactions are re-run.

Reactive constructor

interface ReactiveConstructs {
    createState<T>(value: T | Getter<T>): [get: Getter<T>, set: Setter<T>]
    createReaction(func: () => void)
}

new Reactive(func: (reactive: ReactiveConstructs) => void)

Creates a new Reactive and runs immediately the provided func. During the running of the constructor, Reactive runs any internal calls to createReaction and records any reading of state, which are recorded as dependencies of that specific reaction.

For example, the following will run the reaction every 1000ms when we increase the state

new Reactive((reactive) => {
    let [state, setState] = reactive.createState(12);
    reactive.createReaction(() => console.log(state()))

    setInterval(() => {
        setState(x => x+1)
    }, 1000)
})

createState

type Next<T> = (t: T) => T
type Setter<T> = (t: T | Next<T>) => T
type Getter<T> = () => T
ReactiveConstructs.createState<T>(value: T | Getter<T>): [get: Getter<T>, set: Setter<T>]

Creates a state getter / setter pair such that when setting state, any dependent reaction is rerun. The reactions run immediately, or at the end of a batch when using batchReactions.

The getter always returns a state value The setter accepts a new value or a function to compute the next value

const [state, setState] = reactive.createState(12);

state() // returns 12
setState(13);
setState(x => x + 1);

state

the first function returned by createState is the state function which returns the current value of the state.

setState

The second function returned is setState which accepts a new value or function to update the value. The function will trigger reactions if the value has changed - changed is defined by Revisioned discussed below.

createReaction

ReactiveConstructs.createReaction(func: () => void)

creates a reaction that re-runs when state it depends on changes. It will re-run immediately, or at the end of a batch when using batchReactions.

The reaction function is running once as part of the constructor of Reactive at which dependencies are tracked.

reactive.createReaction(() => {
    console.log(state())
})

Note that only dependencies (state getters) that are actually called during the construction phase are recorded. In the following case, the reaction will track states a and b, but will fail to track state c

const [a, setA] = reactive.createState(true);
const [b, setB] = reactive.createState('abc');
const [c, setC] = reactive.createState('def');

reactive.createReaction(() => {
    if (a())
        b();
    else
        c();
})

batchReactions

Reactive.batchReactions(func: () => void)

Batch reaction enables to update multiple states while computing reactions only once. It is important for performance optimizations, to enable rendering DOM updates once when a component updates multiple states. It is built for the component API to optimize rendering.

let reactive = new Reactive((reactive) => {
    const [a, setA] = reactive.createState(false);
    const [b, setB] = reactive.createState('abc');
    const [c, setC] = reactive.createState('def');
    reactive.createReaction(() => {
        console.log(a(), b(), c())
    })
})

reactive.batchReactions(() => {
    setA(true);
    setB('abcde');
    setC('fghij');
})

toBeClean

Reactive.toBeClean(): Promise<void>

returns a promise that is resolved when pending reactions have run. If there are no pending reactions, the promise will resolve immediately.

reactive.setStateA(12)
reactive.setStateB('Joe')
// waits for reaction to run
await reactive.toBeClean() 

flush

Reactive.flush(): void

In the case of not using batch reactions, reactive will auto batch the reactions and run them async. flush can be used to force the reactions to run sync.

reactive.setStateA(12)
reactive.setStateB('Joe')
// forces reactions to run
reactive.flush() 

Mutable

The mutable module adds support for mutable objects on top of reactive state management. A Mutable object manages an internal revision number (based on Revisioned below) that is updated any time its values, direct or indirect children values change. Once the revision number is updated, the mutable also calls each of its mutable listeners.

The Mutable module is creating proxies for the original objects that are pass-trough, tracking the changes.

Creating a mutable proxy

let obj = {
    a: 1,
    b: {
        c: 2, 
        d: 3
    }, 
    arr: [
        {e: 4}, 
        {e: 5}, 
        {e:6}
    ]};
let mutableObj = mutableObject(obj);

updates that trigger revision update

// update revision of mutableObj
mutableObj.a = 7

// update revision of mutableObj and mutableObj.b
mutableObj.b.c = 7

// update revision of mutableObj and mutableObj.b
mutableObj.b.c = 7

// update revision of mutableObj,  mutableObj.arr and mutableObj.arr[1] 
mutableObj.arr[1].e = 7

// update revision of mutableObj,  mutableObj.arr
mutableObj.arr[1].push({e: 12})

When using Mutable with createState, createState adds a change listener on the mutable object to run reactions when a mutable object changes

let [theState, setTheState] = reactive.createState(mutableObject(obj));

reactive.createReaction(() => console.log(theState()));

// updating the mutable object triggers change listener which triggers state change, and in turn triggers
// the reaction to print to the console.
obj.b.c = 7;

mutableObject

Creates a mutable proxy object or a mutable proxy array over a base object. The notify parent callback is called any time the mutable object changes.

declare function mutableObject<T extends object>(original: T, notifyParent?: ChangeListener): T
declare function mutableObject<T>(original: Array<T>, notifyParent?: ChangeListener): Array<T>

isMutable

Checks if a given object is a mutable proxy.

declare function isMutable(obj: any): obj is object

addMutableListener

Adds another listener to the mutable proxy

type ChangeListener = () => void;
declare function addMutableListener(obj: object, changeListener: ChangeListener)

removeMutableListener

Removes a listener from the mutable proxy

declare function removeMutableListener(obj: object, changeListener: ChangeListener)

Revisioned

The Revisioned subsystem is a system to identify changes in values while supporting both primitives, immutable objects and mutable objects.

  • primitives are considered changed if a !== b
  • immutable objects are considered changed if a !== b
  • mutable objects are considered changed if a[REVISION] !== b[REVISION]

touchRevision

the touchRevision function marks an object as mutable and updates the object revision

declare function touchRevision<T extends object>(value: T): T

checkModified

The check modified function compares two values - a given new value, and an old Revisioned value. The function compares the new value with the old value using the rules above, and returns a new revisioned instance and a is changed flag.

interface Revisioned<T> {
    value: T,
    revision: number
}

declare function checkModified<T>(value: T, oldValue?: Revisioned<T>): [Revisioned<T>, boolean]
1.0.3

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago