npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@oneii3/4iv

v0.0.12

Published

A novel, composable, unopinionated FRP library.

Downloads

3

Readme

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