@oneii3/4iv
TypeScript icon, indicating that this package has built-in type declarations

0.0.12 • Public • Published

4IV

4IV <pronounced "four four"> is a library for reactive programming, inspired by react, to ease the stress of composing data which changes over time. It takes the design decisions around react's state and effects, and applies it to general functions, decoupling the state from the view completely.

Its designed to be flexible and interoperable to allow for a progressive integration into your current apis.

Features

  • 🔬 Tiny (35k unzipped)
  • Interoperable
  • 🧩 Composable
  • 🙌 Minimalistic and unopinionated
  • 🔄 2 way binding
  • 🤖 Typescript Support
  • 🤷🏽‍♂️ It works!

Demos

//TBD

Installation

yarn

yarn add @oneii3/4iv

npm

npm i @oneii3/4iv --save

Basics

Concepts

On the surface, 4iv is just another reactive programming library. Where it differs is that the abstractions over state in other libraries is too opinionated, and makes coordinating and composing state more difficult that it should be.

Under the hood, 4iv uses a class called Action which implements a modified Observer pattern.

interface Action<T> {
    value: T;
    constructor(initialValue: T, effects: ((next: T, prev: T) => void)[]);
    next(value: T): this;
    tick(): this;
    then(effect: (next: T, prev: T) => void): this;
    stop(effect: (next: T, prev: T) => void): this;
    catch(effect: (error: Error | any, next: T, prev: T) => any): this;
}

Every call to the then method adds a new effect to the Action. When the next method is subsequently called with a new value, the Action's effects are signaled that a change has occurred, supplying the effects with the current and previous value of the action.

Example:

let action: Action<number> = new Action(5);
let effect = (next, prev) => {
    console.log(prev, next);
};
action.then(effect).catch((e) => console.log("Error: " + e.message));
action.next(6); //logs: 5, 6
action.stop(effect);
action.next(7); // noop
action.next(new Promise((res, rej) => rej(new Error("Some Error")))); //logs: "Error: Some Error"

It's not very different from an Observable. In fact, it essentially is an observable with a slightly modified interface. What 4iv does different is abstract over this interface in order to provide easier, more coherent compositions of these observables. These abstractions allow 4iv to remain small, without losing the ablity to construct complex compositions of the Actions.

All methods of the Action return the Action instance, which provides an action with a chainable api.

action
    .then((value) => someMEthod(value + 1))
    .next(9)
    .next(10);

NOTE: Actions may only contain a single error handler (supplied via .catch() method). Subsequent calls to catch will override the previous handler. The error handler is called whenever a promise is rejected or if an effect throws an error.

Primitives

😀 State

State is a value that has a functional interface for changing it over time.

State is a functional interface over an Action, which also simultaneously maintains the interface of the underlying value.

type state<T> = (value: T, setter?: (...args: any[]) => any) => State<T>;
type State<T> = T & ((T) => T) & Action<T>;

You create one using the state function.

import { state } from "@oneii3/4iv";

let a: State<number> = state(1);

Since a state variable shares the same interface as the value that constructed it. This means, the state of a number, will still work just like any other number in your code. It just additionally acts as a setter for the next or then method of the underlying Action when it is called like as function, with either a new value or an effect respectively.

import { state } from "@oneii3/4iv";

let a = state(1);

assert(typeof a == "function");
assert(a instanceof Number);
assert(a == 1);
assert(a + 3 == 4);
a(2);
assert(a == 2);
assert(a(3) == 3);

When a state variable is called with a promise, it will internally await the value returned from the promise.

a(
    new Promise((res) => {
        setTimeout(() => {
            res(10);
        }, 1000);
    })
); //a implicitly becomes 10 in 1 sec

Because the values supplied to a state variable may change at any time, to capture the values as they change, a function may also be supplied as an argument to the state setter. This adds the function to the state's effect queue. This effect is called whenever the state variable changes values (more on this below).

a((next, last) => console.log(next, last));
a(3); //logs: 3, 2

When using a state variable as a function, it returns the value supplied. So to bind one state to another, simply compose them.

let b = state();
let c = a(b(1));
assert(a == 1);
assert(b == 1);
assert(c == 1);

Two state variables that are equal to the same value, are not equal to one another.

assert(a != b);

You may define a specialized setter for a state by supplying the state function with an optional setter as a second argument.

let b = state(1, (a, b) => a + b);
b(1, 4);
assert(b == 5);

The result of the setter function will become the value supplied to the next method of the underlying Action.

😃 Product

type product<T, U> = (
    value: (last: T) => T,
    dependencies: State<U>[]
) => Product<T>;

type Product<T> = State<T>;

A product is a state variable that is derived from two or more other state variables. When any of the input variables change, it recomputes its value. The changes are tracked by the values supplied to the depencency array and not the values used in the value function.

import { state, product } from "@oneii3/4iv";

let a = state(1);
let b = state(2);
let c = product(() => a + b, [a, b]);
assert(c == 3);
a(2);
assert(c == 4);

Since it is also a state variable itself, the value can also be changed directly. It will also still recompute once any of its inputs change.

c(0);
assert(c == 0);

NOTE: setting the product in this way will not affect the input variables in any way. assert(a == 2); assert(b == 2);

😅 Byproduct

type byproduct<T> = (
    product: (value?: T, lastValue?: T) => T,
    quotient: (value?: T, lastValue?: T) => void,
    deps: State<T>[] = []
) => Byproduct<T>;
type Byproduct<T> = State<T>;

A Byproduct is a bidirectional Product. What this means is that changing either its dependencies or the result of a byproduct, will cause either the product effect or quotient effect respectively in response to the change.

This is especially useful when data needs to be derived back to the input dependencies.

let x = state(1);
let y = state(2);
let z = byproduct(
    (next, last) => x + y,
    (next, last) => x(z - y),
    [x, y]
);
assert(z == 3);
assert(x == 1);
z(5);
assert(z == 5);
assert(x == 3);

In the above example, the byproduct is able to maintain the constraints of all of its values, no matter which state gets changed. This becomes extremely valuable when components can be editted from two different sources, which dictate either the pieces or the derived value of a stateful value.

😄 Effect

Effects are simply the functions called in response to a state change. The effect function however is sugar for composing state variables with an effect callback and calling it immediately.

import { state, effect } from "@oneii3/4iv";

let a = state(1);
let b = state(2);
effect(() => console.log({ a, b }), [a, b]); //logs: { a: 1, b: 2 }
//same as: a(b(() => console.log({ a, b }))).call()

a(2); //logs: { a: 2, b: 2 }
b(3); //logs: { a: 2, b: 3 }
a(2); //doesnt log

Since effects are called immediately, you can defer execution of the effect until the dependencies change for the first time, call effect.defer()

effect.defer(() => console.log({ a, b }), [a, b]); //nothing
a(10); // logs: { a: 10, b: 3 }

If you supply a product as a dependency, the effect will be triggered when either the product is set directly or being recomputed.

let c = product(() => `values: "${a} ${b}"`, [a, b]);
effect.defer(() => console.log(c), [c]);

a(b("hello"));
//logs: `values: "10 hello"`
//logs: `values: "hello hello"`

c("goodbye");
//logs: `goodbye`

b("world");
//logs: `values: "hello world"

Effect Nesting

Unlike react, There is no restriction to where you may place your state and effects. This means that you may define an effect within the body of an effect.

effect(()=>{
    let c = state("Higher Order State");
    let d = state("More State");
    effect(()=>{
        console.log("Higher Order Effects")
    }, [c,d])
},[a,b])

These "Instance Effects/State", if not used responsibly, can pollute your effect chains, and cause infinite loops and runtime slowness. Be careful not to needlessly nest these structures.

😁 Event

event<T extends EventCallback> = ( callback: T ) => [
    State<T>,
    State<[
        State<T[arguments]>,
        State<T[return]>
        ]>
]
EventCallback = (...args: any) => any;

An event is a state variable which triggers its effects when the supplied callback is used.

import { event } from "@oneii3/4iv";

let [add, state] = event((a, b) => a + b);

effect(() => {
    let [args, result] = state;
    console.log(`${args} -> ${result}`);
}, [state]);

add(1, 2); //logs 1,2 -> 3

Comparisons

// TBD


API

Action

Instance

Action.prototype.next<T>(value: T): this

Used to change the value of the action; triggers effects if present.

Action.prototype.then<T>(effect: Effect<T>): this

Adds effect to effects set for Action.

Action.prototype.stop<T>(effect: Effect<T>): this

Removes effect from effects set of Action.

Action.prototype.toJson(): string

Returns a json string of action.

Static

Action.all<T>(state: State<T> | Action<T>, effect: Effect<T>): void

Binds the supplied state variables to the supplied effect. (adds effect to each state variables' effect set). Will call effect if, and only if, all of the states have changed at least once.

Action.any<T>(state: State<T> | Action<T>, effect: Effect<T>): void

Binds the supplied state variables to the supplied effect. Will call effect if any of the state variables change.

Action.once<T>(state: State<T> | Action<T>, effect: Effect<T>): void

Binds the supplied state variables to the supplied effect. Will call effect only once when any of the state variables change, then removes effect from each state variable.

Action.onceAll<T>(state: State<T> | Action<T>, effect: Effect<T>): void

Binds the supplied state variables to the supplied effect. Will call effect if, and only if, all of the states have changed at least once, then removes effect from each state variable.

Action.fromJson(json: string): State<T>

Turns json data into an Action instance.

Action.fromObject(obj: Record<string, any>): State<T>

Turns an Action record (object which follows json config for Action) into a state variable.


state

Instance

(Inherits from Action.prototype)

Static

state.of( emitter: any, event: string ) : state<any>

state.of( event: string ) : state<any>

Creates a state variable from an event emitter (objects which possess an .on or .addEventListener method). If only the name of the event is provided, the emitter will default to window.document.


effect

Static

effect.defer<T>(e: Effect<State<T>[]>, deps?: State<T>[], all?: boolean)

effect.filter<T>(e: Effect<State<T>[]>, deps?: State<T>[])


Guides

Recipes

Readme

Keywords

none

Package Sidebar

Install

npm i @oneii3/4iv

Weekly Downloads

0

Version

0.0.12

License

MIT

Unpacked Size

34.8 kB

Total Files

25

Last publish

Collaborators

  • ja.superior