Skip to content

e280/strata

Repository files navigation



⛏️ strata

get in loser, we're managing state

📦 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



🍋 strata signals

reactive bundles of joy

import {signal, derived, effect, batch} from "@e280/strata"

🚦 signal

  • 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)

🚦 derived

  • 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

  • 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.

🚦 batch

  • optimize multiple writes into one fat update
    // call downstream effects only once
    batch(() => {
      $count(4)
      $count(5)
      $count(6)
    })

🚦 types

  • Signal<Value> — it's a signal fn
  • Derived<Value> — it's a derived fn
  • Valuable<Value> — could be Signal<Value> or Derived<Value>



🍋 strata prism

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

🔮 prism and lenses

  • 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.state is a cloned mutable snapshot with chill typings
    snacks.state.peanuts // 8
    person.state.name // "chase"
    
    snacks.state.peanuts++
      // ⛔ attempted state mutation: silently ignored
  • lens.frozen provides a deep-frozen immutable snapshot with strict typings
    snacks.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"))

🔮 chrono for time travel

  • 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 💀

🔮 persistence to localStorage

  • 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!
    })
    • store type is compatible with @e280/kv
  • cross-tab sync (load on storage events)
    store.onStorageEvent(vault.load)
  • initial load
    await vault.load()



🍋 strata wait

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.

⌛ good things come to those who wait

  • 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!

⌛ waitFormal is persnickety belt-and-suspenders mode

  • 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")
    })

⌛ wait, there's more

  • 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}`,
    })



🍋 strata tracker

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.

🪄 invent your own novel state concept

  • 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

🪄 integrate your ui framework for auto-rerendering

  • use tracker.observe to check what is touched by a fn
  • use tracker.subscribe to subscribe to the seen items that observe returns
  • see the source code



🍋 react bindings

⚛️ setup your strata.ts module

import * as react from "react"
import {reactBindings} from "@e280/strata"

export const {
  component,
  useTracked,
  useOnce,
  useSignal,
  useDerived,
} = reactBindings(react)

⚛️ component enables fully automatic reactive re-rendering

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>
})

⚛️ useTracked for a manual hands-on approach (plays nicer with hmr)

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>
}

⚛️ useSignal for local component state (and useDerived)

import {useSignal} from "./strata.js"

export const MyCounter = () => {
  const $count = useSignal(0)
  const add = () => $count($count() + 1)
  return <button onClick={add}>{$count()}</button>
}



🧑‍💻 strata is by e280

free and open source by https://e280.org/
join us if you're cool and good at dev

About

⛏️ incredi state management

Topics

Resources

License

Stars

Watchers

Forks

Contributors