📦 npm install @e280/strata
🧙♂️ probably my tenth state management library, lol
💁 it's all about rerendering ui when data changes
🚦 signals — ephemeral view-level state
🌳 tree — persistent app-level state
🪄 tracker — reactivity integration hub
Tip
incredibly, signals and trees are interoperable.
that means, effects and computeds are responsive to changes in tree state.
import {signal, effect, computed} from "@e280/strata"
- create a signal
const count = signal(0)
- read a signal
count() // 0
- set a signal
count(1)
- set a signal, and await effect propagation
await count(2)
- signals hipster fn syntax
count() // get await count(2) // set
- signals get/set syntax
count.get() // get await count.set(2) // set
- signals .value accessor syntax
value pattern is nice for this vibe
count.value // get count.value = 2 // set
count.value++ count.value += 1
- effects run when the relevant signals change
effect(() => console.log(count())) // 1 // the system detects 'count' is relevant count.value++ // 2 // when count is changed, the effect fn is run
- computed signals are super lazy
they only run if and when you get the valueconst tenly = computed(() => { console.log("recomputed!") return count() * 10 }) console.log(tenly()) // "recomputed!" // 20 await count(3) console.log(tenly.value) // "recomputed!" // 30
- single-source-of-truth state tree
- immutable except for
mutate(fn)
calls - localStorage persistence, cross-tab sync, undo/redo history
- no spooky-dookie proxy magic — just god's honest javascript
- compatible with signals, but different
- better stick to json-friendly serializable data
import {Trunk} from "@e280/strata" const trunk = new Trunk({ count: 0, snacks: { peanuts: 8, bag: ["popcorn", "butter"], }, }) trunk.state.count // 0 trunk.state.snacks.peanuts // 8
- ⛔ informal mutations are denied
trunk.state.count++ // error is thrown
- ✅ formal mutations are allowed
await trunk.mutate(s => s.count++)
- it's a lens, make lots of them, pass 'em around your app
const snacks = trunk.branch(s => s.snacks)
- run branch mutations
await snacks.mutate(s => s.peanuts++)
- array mutations are unironically based, actually
await snacks.mutate(s => s.bag.push("salt"))
- you can branch a branch
- on the trunk, we can listen deeply for mutations within the whole tree
trunk.watch(s => console.log(s.count))
- whereas branch listeners don't care about changes outside their scope
snacks.watch(s => console.log(s.peanuts))
- watch returns a fn to stop listening
const stop = trunk.watch(s => console.log(s.count)) stop() // stop listening
- simple setup
const {trunk} = await Trunk.setup({ version: 1, // 👈 bump whenever your change state schema! initialState: {count: 0}, })
- uses localStorage by default
- it's compatible with
@e280/kv
import {Kv, StorageDriver} from "@e280/kv" const kv = new Kv(new StorageDriver()) const store = kv.store<any>("appState") const {trunk} = await Trunk.setup({ version: 1, initialState: {count: 0}, persistence: { store, onChange: StorageDriver.onStorageEvent, }, })
- first, put a
Chronicle
into your state treeconst trunk = new Trunk({ count: 0, snacks: Trunk.chronicle({ peanuts: 8, bag: ["popcorn", "butter"], }), })
- 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
- second, make a
Chronobranch
which is like a branch, but is concerned with historyconst snacks = trunk.chronobranch(64, s => s.snacks) // \ // how many past snapshots to store
- mutations will advance history (undoable/redoable)
await snacks.mutate(s => s.peanuts = 101) await snacks.undo() // back to 8 peanuts await snacks.redo() // forward to 101 peanuts
- you can check how many undoable or redoable steps are available
snacks.undoable // 2 snacks.redoable // 1
- chronobranch can have its own branches — all their mutations advance history
- plz pinky-swear right now, that you won't create a chronobranch under a branch under another chronobranch 💀
-
import {tracker} from "@e280/strata/tracker"
- all reactivity is orchestrated by the
tracker
- if you are integrating a new state object, or a new view layer that needs to react to state changes, just read tracker.ts
free and open source by https://e280.org/
join us if you're cool and good at dev