📦 npm install @e280/strata
✨ it's basically about automagically rerendering ui when data changes
🦝 powers auto-reactivity in our view library @e280/sly
🧙♂️ probably my tenth state management library, lol
🧑💻 a project by https://e280.org/
🚦 #signals, sweet little bundles of state
🔮 #prism, bigger centralized state trees
⌛ #wait, async state helpers (think loading spinners)
🪄 #tracker, agnostic reactivity integration hub
⚛️ #react, optional bindings for react
reactive bundles of joy
import {signal, derived, effect, batch} from "@e280/strata"- a signal holds a value
const $count = signal(1)
we kinda like the
$convention for signals - read the signal
$count() // 1
- write the signal
$count(2)
- combine signals like a formula
const $alpha = signal(1) const $bravo = signal(10) const $product = derived(() => $alpha() * $bravo())
- it automatically updates
$product() // 10 $alpha(2) $product() // 🪄 20
- btw,
deriveds are lazy, only run the fn when demanded.
also, they have a.dispose()fn if you need to stop them.
- effects run immediately, then again when relevant signals change
effect(() => console.log($count())) // 2 // the system detects '$count' is relevant $count(3) // 3 // when $count is changed, the effect fn is run
- btw,
effect returns a dispose fn if you need to stop it.
- optimize multiple writes into one fat update
// call downstream effects only once batch(() => { $count(4) $count(5) $count(6) })
Signal<Value>— it's a signal fnDerived<Value>— it's a derived fnValuable<Value>— could beSignal<Value>orDerived<Value>
persistent app-level state
- single-source-of-truth state tree
- no spooky-dookie proxy magic — just god's honest javascript
- immutable except for
mutate(fn)calls - use many lenses, efficient reactivity
- chrono provides undo/redo history
- persistence, localstorage, cross-tab sync
- import prism
import {Prism} from "@e280/strata"
- prism is a state tree
const prism = new Prism({ snacks: { peanuts: 8, bag: ["popcorn", "butter"], person: { name: "chase", incredi: true, }, }, })
- create lenses, which are views into state subtrees
const snacks = prism.lens(state => state.snacks) const person = snacks.lens(state => state.person)
- you can lens another lens
lens.stateis a cloned mutable snapshot with chill typingssnacks.state.peanuts // 8 person.state.name // "chase" snacks.state.peanuts++ // ⛔ attempted state mutation: silently ignored
lens.frozenprovides a deep-frozen immutable snapshot with strict typingssnacks.frozen.peanuts // 8 (readonly) person.frozen.name // "chase" (readonly) snacks.frozen.peanuts++ // ⛔ attempted frozen mutation: throw errors
- only formal mutations can actually change state
snacks.mutate(state => state.peanuts++) // ✅ formal mutations to change state snacks.state.peanuts // 9
- array mutations are unironically based, actually
snacks.mutate(state => state.bag.push("salt"))
- import stuff
import {Chrono, chronicle} from "@e280/strata"
- create a chronicle in your state
const prism = new Prism({ // chronicle stores history // 👇 snacks: chronicle({ peanuts: 8, bag: ["popcorn", "butter"], person: { name: "chase", incredi: true, }, }), })
- big-brain moment: the whole chronicle itself is stored in the state.. serializable.. think persistence — user can close their project, reopen, and their undo/redo history is still chillin' — brat girl summer
- create a chrono-wrapped lens to interact with your chronicle
const snacks = new Chrono(64, prism.lens(state => state.snacks)) // 👆 // how many past snapshots to store
- mutations will advance history, and undo/redo works
snacks.mutate(s => s.peanuts = 101) snacks.undo() // back to 8 peanuts snacks.redo() // forward to 101 peanuts
- check how many undoable or redoable steps are available
snacks.undoable // 1 snacks.redoable // 0
- you can make sub-lenses of a chrono, all their mutations advance history too
- plz pinky-swear right now, that you won't create a chrono under a lens under another chrono 💀
- import prism
import {Vault, LocalStore} from "@e280/strata"
- create a local storage store
const store = new LocalStore("myAppState")
- make a vault for your prism
const vault = new Vault({ prism, store, version: 1, // 👈 bump this when you break your state schema! })
storetype is compatible with@e280/kv
- cross-tab sync (load on storage events)
store.onStorageEvent(vault.load)
- initial load
await vault.load()
tiny async state helpers
it's about states like pending, ok, err.
wait extends stz's ok/err toolkit, and it's mostly for showing little loading spinners in your ui.
- import stuff
import {ok, err, nap} from "@e280/stz" import {wait, waitFormal} from "@e280/strata"
- wrap any async operation and get a "Waiter"
// wrap any async operation in a fancy wait const $wait = wait(async() => { await nap(100) if (Math.random() > 0.5) return 123 else throw new Error("bad luck!") })
- btw you can pass a promise instead of an async fn
- check if it's done
console.log($wait().done) // false -- sorry bro, its not ready yet
- okay, we can actually await for the result
const result = await $wait.result if (result.ok) console.log(result.value) // 123 else console.error(result.error) // Error: bad luck!
- you can get super explicit about the types
const $wait = waitFormal<number, "unlucky" | "bad roll">(async() => { if (Math.random() > 0.5) return ok(123) if (Math.random() < 0.01) return err("unlucky") else return err("bad roll") })
- maker
makeWait<number>() // pending makeWait(ok(123)) makeWait(err("uh oh"))
- status checkers
isWaitPending($wait()) isWaitDone($wait()) // ok or err isWaitOk($wait()) isWaitErr($wait())
- value grabbers
waitGetOk($wait()) // 123 | undefined waitNeedOk($wait()) // 123 (or throws an error)
waitGetErr($wait()) // "bad roll" | undefined waitNeedErr($wait()) // "bad roll" (or throws an error)
- quick selector
const text = waitSelect($wait(), { pending: () => "still loading...", ok: value => `ready: ${value}`, err: error => `ack! ${error}`, })
reactivity integration hub
import {tracker} from "@e280/strata/tracker"this is the inner sanctum of strata. use the tracker to jack into the reactivity system, you can make anything fully strata-compatible and you'll be reactin' and triggerin' with the best of 'em. the tracker is also what you'll need if you're trying to create bindings for your own frontend framework to trigger your ui to rerender and stuff.
- let's invent a very simple thing, so you can see how simple the tracker really is.
export class BoomerSignal<Value> { constructor(private value: Value) {} get() { tracker.read(this) // 🪄 inform tracker our thing was accessed return this.value } set(value: Value) { this.value = value tracker.write(this) // 🪄 inform tracker our thing was changed } }
- boom, that's it! now we have a new reactive thing we can use, and it'll rerender our ui or whatever.
const $count = new BoomerSignal(1) effect(() => console.log($count.get())) // 1 $count.set(2) // 2
- use
tracker.observeto check what is touched by a fn - use
tracker.subscribeto subscribe to the seen items thatobservereturns - see the source code
import * as react from "react"
import {reactBindings} from "@e280/strata"
export const {
component,
useTracked,
useOnce,
useSignal,
useDerived,
} = reactBindings(react)import {signal} from "@e280/strata"
import {component} from "./strata.js"
const $count = signal(0)
export const MyCounter = component(() => {
const add = () => $count($count() + 1)
return <button onClick={add}>{$count()}</button>
})import {signal} from "@e280/strata"
import {useTracked} from "./strata.js"
const $count = signal(0)
export const MyCounter = () => {
const count = useTracked(() => $count())
const add = () => $count($count() + 1)
return <button onClick={add}>{count}</button>
}import {useSignal} from "./strata.js"
export const MyCounter = () => {
const $count = useSignal(0)
const add = () => $count($count() + 1)
return <button onClick={add}>{$count()}</button>
}free and open source by https://e280.org/
join us if you're cool and good at dev