Signal-based Reactive Data Modeling and Management.
This is a Reactivity Primitives Library, a uniform system that tries to comprehend industry standards.
It supports two kind of Signals (EventSignal and ClosureSignal), gives utils to ease reactivity rather than piping.
import { State, StateArray, Notifier } from "@denshya/reactive"
const blog = {
articles: new StateArray([{
title: "",
description: ""
}]),
user: new State({
id: 1,
name: "FrameMuse"
}),
admin: {
enabled: new State(false),
save: new Notifier,
cancel: new Notifier,
}
}
blog.articles.push({
title: "How to use `Reactive`",
description: "Follow our guides..."
})
blog.admin.enabled.set(true) // Active admin UI.
blog.admin.save.dispatch() // Buttons triggers saving.
blog.user.set({ id: 2, name: "Denshya" }) // Replace user.
blog.user.$.name.set("Reactive") // Partial change.Note
Updating a state doesn't cause updates to other ones.
Tip
It's similar but more powerful than https://tanstack.com/store/latest/docs/overview
Using event-based signals
import { State } from "@denshya/reactive"
const balance = new State(0)
const income = new State(100)
const debt = new State(500)
const salary = income.to(it => it / 2)
salary.subscribe(salary => balance.set(it => it + salary))
const netWorth = balance.to(balance => balance - debt)Using closure-based ones
import { State } from "@denshya/reactive"
const balance = new State(0)
const income = new State(100)
const debt = new State(500)
const salary = State.capture(() => income.use() / 2)
State.capture(() => {
balance.set(balance.get() + salary.use())
})
const netWorth = State.capture(() => balance.use() - debt.use())State modes are interchangeable, so you can use both together.
to method is map function under a different name because state.map(x => x.map(e => e)) seems a bit weird.
And it looks neat as Tacit programming - string.to(Number)
It creates a new State instance, transforms the value and assign it to the new instance.
It is useful when you want to select a value, but save reactivity:
import { State } from "@denshya/reactive"
const ypx = new State("15px")
const y = ypx.to(parseFloat)
ypx.set("16px") // Will set `y` to `16`.
y.set(15) // Will not affect `ypx`.It exposes set method that hooks to places where Signal-like structures required
It is useful when you want to fit "source" (or "sink") from where a new value is coming to a desired one:
import { State } from "@denshya/reactive"
const pointerX = new State(0)
window.addEventListener("pointermove", pointerX.from(event => event.x))
window.when("pointermove").subscribe(pointerX.from(event => event.x))
const event = new State(new PointerEvent(""))
event.sets(pointerX.from(event => event.x))This literally says "event sets value from event.currentTarget.value".
import { State } from "@denshya/reactive"
const app = new State({ user: { name: "test" } })
// Regular Access
app.get().user.name
app.current.user.name
// Observable Access
app.$.user.$.name.subscribe(console.log) // Logs `app.user.name` changes.
// Usage of `$` is cached and an observable for accessed property only created when first accessed.
app.$.user === app.$.user // trueimport { State } from "@denshya/reactive"
/** Captures every `use()` that appear in the closure and subscribes to their updates produces new value. */
State.capture(() => state1.use() + state2.use())
/** Combines several state-like values into one with a strategy. */
State.combine([state1, state2], (state1, state2) => state1 + state2)
/** Finds all (shallow) values in `Record` or `Array` and outputs it `State` with unwrapped values. */
State.collect([state1, state2]) // Reduces to e.g. `State<[number, number]>` from `[State<number>, State<number>]`
State.collect({ foo: state1, bar: state2 }) // Reduces to `State<{ foo: number, bar: number }>`
/** Builds a string state from string template of observables. */
State.f`display: ${style.$.display}; opacity: ${1}`
/** Creates `State` from a plain value or forks from existing one. */
State.from(...)
/** Unwraps any Signal-like structure. */
State.get(...)
/** Uses any Signal-like structure (even third-party) as `ClosureSignal` - can be used in `State.capture`. */
State.use(signalLike)
/** Subscribes to any Signal-like structure. */
State.subscribe(signalLike, () => {...})
/** Subscribes to any Signal-like structure and invokes `callback` immediately once. */
State.subscribeImmediate(signalLike, () => {...})An array representation of State, it has more convenient at and push methods, and new one delete.
import { State, StateArray } from "@denshya/reactive"
const array = new StateArray([1,2,3])
array.subscribe(console.log) // Logs `array` changes.
array.set([1,2,3,4])
array.push(5) // Triggers update.
array.at(2) // Returns an observable that reflects the value at desired index.
// Index can also be observable.
const index = new State(1)
array.at(index)
array.delete(2) // Triggers update.
console.log(array.get()) // [1,2,4,5]However, you can still use State with arrays by using $[index], it will still work but not so comfortable.
Finite State Machine, based on Signal and Emitter.
const character = {
state: new StateFSM<"idle" | "walking" | "running">("idle")
}
state.when("idle").subscribe(() => {...})
state.when("walking").subscribe(() => {...})
state.subscribe(state => state === "idle" && idle())
state.set("walking")If reactive source is third-party, you can use .from method that is present on many class constructors
like State, StateArray, etc. to convert the third-party to canonical.
Something like StateArray.from(state) can be used to copy values too.
const someState = new State([1,2,3])
const stateArray = StateArray.from(someState) // => [1,2,3]import { State } from "@denshya/reactive"
const state = State.from({
#value: 1,
subscribe(next: (value: number) => void) {
const id = setInterval(() => next(this.#value + 1), 1000)
return () => clearInterval(id)
}
})
state // => `State` instance.State (and other) has hidden toJSON method, which outputs actual value for serialization.
import { State, StateArray } from "@denshya/reactive"
const bool = new State(true)
const string = new State("text")
const record = new State({foo:"bar"})
const array = new StateArray([1,2,3])
JSON.stringify({ bool, string, record, array })=>
{
"bool": true,
"string": "text",
"record": { "foo" : "bar" },
"array": [1, 2, 3]
}All subscriptions follow WICG Observable API proposal and thus return object (record) with callback to unsubscribe.
const state = new State(123)
const subscription = state.subscribe(() => {...})
subscription.unsubscribe()However, this library handles multiple variations of subscriptions/unsubscribe if it's reasonable.
e.g. State.subscribe(signalLike, () => {...}) will return subscription just like in the example above,
even if signalLike.subscribe returns different one.
Dispatcher for single messages. Useful for building custom Signal-like structures.
Is a Messager but only for empty messages. Use for semantics.
A key-based event messager, usually known as Event Emitter.
import { Emitter } from "@denshya/reactive"
interface Events {
add(id: number): void
}
const emitter = new Emitter<Events>
emitter.when("add").subscribe(console.log) // Logs `add` events.
emitter.dispatch("add", 1)