respo

    A tiny virtual DOM library

    virtual-dom
    Download zip
    Author
    Version
    0.3.4
    License
    Apache-2.0
    Last updated
    4 months ago
    Downloads
    2K

    #Respo in MoonBit

    A tiny virtual DOM library ported from Respo.rs.

    Core ideas:

    • Immutable data by design - Store and component states must be immutable. Updates create new values using record update syntax ({ ..old, field: new_value }). Use Ref[T] for mutable references at the application level
    • Hot reload friendly for better dev experience
    • Embraces types and autocompletion
    • States are stored in a single atom, which is a tree structure with a concept like cursor
    • Uni-directional data flow, dispatching is only allowed when user events occur
    • Effects are for DOM updates, rather than dispatching actions
    • Physical equality optimization - Since data is immutable, unchanged references are skipped during re-rendering

    This project is in beta stage. APIs and structure are relatively stable.

    #Immutable Data Requirements

    Respo requires all state to be immutable. This is not a style preference — it is required for correctness of the render loop.

    #How the render loop works

    Every animation frame, Respo checks whether the store has changed before re-running the renderer:

    raf_loop: if physical_equal(store.val, prev_store) → skip (same heap object) if store.val == prev_store → skip (structurally equal) else → diff vdom, patch DOM

    Both checks are only reliable when the store is genuinely immutable (no mut struct fields).

    #The mut field pitfall

    If Store has mut fields and update() mutates them in place, then { ..store.val } creates a new struct header, but both store.val and prev_store share the same field values via the mutation. The == check returns true and the DOM is never updated — even though the data changed.

    // ❌ WRONG — mut field, in-place mutation
    struct Store {
    mut count : Int // mut field!
    } derive(Eq)

    fn Store::update(self : Store, op : ActionOp) -> ActionOp? {
    self.count 1 // mutates in place — prev_store sees this too
    None
    }

    // caller (broken pattern):
    ignore(app.store.val.update(op))
    app.store.val = { ..app.store.val } // shallow copy, but fields already mutated
    // Result: store.val == prev_store → render skipped → DOM frozen

    // ✅ CORRECT — no mut, update returns new store
    struct Store {
    count : Int
    } derive(Eq)

    fn Store::update(self : Store, op : ActionOp) -> (Store, ActionOp?) {
    match op {
    Increment => ({ ..self, count: self.count + 1 }, None)
    }
    }

    // caller (correct pattern):
    let (new_store, maybe_op) = app.store.val.update(op)
    app.store.val = new_store
    // Result: physical_equal fails (new struct) → diff runs → DOM updated

    #MoonBit record update syntax

    MoonBit's { ..self, field: new_value } creates a new struct with one field replaced. Use it freely for nested updates:

    { ..self, ui: { ..self.ui, username: value } }

    #Debugging render skips

    Enable Respo's built-in debug logging to diagnose unexpected render skips:

    @respo.set_debug_mode(true) // add near app startup, remove after debugging

    Console output:

    • [Respo Debug] Render: skipped - store reference unchangedphysical_equal short-circuit (expected after no-op dispatches)
    • [Respo Debug] Render: skipped - store logically equal== short-circuit; if unexpected, check for mut fields in Store
    • [Respo Debug] Render: starting render cycle — diff is running
    • [Respo Debug] Render: N DOM changes — patches applied to DOM

    #Usage

    moon add tiye/respo

    To use Respo, start with the boilerplate at https://github.com/Respo/respo-moonbit-workflow

    #License

    Apache-2.0

    ObscureState

    type ObscureState

    MoonBit does not allow trait object with Selfs, use ffi js value to bypass.

    RespoApp

    pub(all) struct RespoApp[Model] {
    store :
    Ref
    [Model]
    storage_key : String
    mount_target :
    Node

    }

    get basic App structure

    RespoApp::backup_model_beforeunload

    fn[Model : ToJson] RespoApp::backup_model_beforeunload(self : RespoApp[Model]) -> Unit

    backup store to local storage before unload

    RespoApp::render_loop

    fn[Model : Eq, ActionOp, G] RespoApp::render_loop(self : RespoApp[Model], renderer : () ->
    RespoNode
    [ActionOp, G] raise
    RespoCommonError
    , dispatch_action : (ActionOp) -> Unit raise
    RespoCommonError
    ) -> Unit

    RespoStatesTree

    pub(all) struct RespoStatesTree {
    backup : Json?
    cursor :
    Vector
    [String]
    branches :
    HashMap
    [String, RespoStatesTree]
    // private fields
    }

    Respo maintains states in a tree structure, where the keys are strings, each child component "picks" a key to attach its own state to the tree, and it dispatches events to global store to update the state.

    This is an immutable data structure - all updates return new trees.

    RespoStatesTree::cast_branch

    Cast the data in the branch to the specified type. If data is not present but backup exists, restore from backup.

    RespoStatesTree::local_pair

    local state in component could be None according to the tree structure Returns (state, cursor)
    let (state, cursor) = states.local_pair();

    RespoStatesTree::path

    RespoStatesTree::pick

    fn RespoStatesTree::pick(self : RespoStatesTree, name : String) -> RespoStatesTree

    Pick a branch from the tree by name, returns a new tree representing that branch.

    RespoStatesTree::set_in

    Immutably update the tree at the specified cursor path. Returns a new tree with the update applied.

    RespoUpdateState

    pub(all) struct RespoUpdateState {
    cursor : Array[String]
    data : ObscureState?
    backup : Json?
    }

    framework defined action for updating states branch

    StateRef

    pub struct StateRef[T] {
    cell :
    Ref
    [T]
    }

    StateRef wraps a mutable Ref while serializing to an empty JSON object. Use it for ephemeral fields that should not affect persisted snapshots.
    impl Default for StateRef[T]
    impl Eq for StateRef[T]
    impl Hash for StateRef[T]
    impl Show for StateRef[T]
    impl ToJson for StateRef[T]

    StateRef::borrow

    fn[T] StateRef::borrow(self : StateRef[T]) ->
    Ref
    [T]

    Returns the underlying Ref so callers can mutate the payload directly.

    StateRef::get

    fn[T] StateRef::get(self : StateRef[T]) -> T

    Get the current value (read-only)

    StateRef::new

    fn[T] StateRef::new(value : T) -> StateRef[T]

    StateRef::set

    fn[T] StateRef::set(self : StateRef[T], value : T) -> Unit

    Set a new value

    StateRef::update

    fn[T] StateRef::update(self : StateRef[T], f : (T) -> T) -> Unit

    Update the value using a function

    code_fonts

    let code_fonts : String

    collect_global_handlers

    Collect all global event handlers from the virtual DOM tree Returns an array of handler functions with their coordinates

    debug_event

    fn debug_event(event_type : String, details : String) -> Unit

    Log an event (only when debug mode is enabled)

    debug_log

    fn debug_log(message : String) -> Unit

    Log a debug message (only when debug mode is enabled)

    debug_render

    fn debug_render(message : String) -> Unit

    Log a render cycle (only when debug mode is enabled)

    debug_state_change

    fn[T : Show] debug_state_change(label : String, old_state : T, new_state : T) -> Unit

    Log a state change (only when debug mode is enabled)

    debug_store_update

    fn[T : Show] debug_store_update(action : T) -> Unit

    Log store update (only when debug mode is enabled)

    default_fonts

    let default_fonts : String

    dispatch_global_event

    fn[T, G] dispatch_global_event(event : G, tree :
    RespoNode
    [T, G], dispatch :
    DispatchFn
    [T]) -> Unit raise
    RespoCommonError

    Dispatch a global event to all registered handlers in the tree

    fancy_fonts

    let fancy_fonts : String

    is_debug_mode

    fn is_debug_mode() -> Bool

    Check if debug mode is enabled

    mark_need_rerender

    fn mark_need_rerender() -> Unit

    Signal that the store has changed and a re-render should be scheduled.

    Call this after assigning app.store.val = new_store to trigger the next animation-frame render cycle.

    memo_once1

    fn[K : Eq, V] memo_once1(f : (K) -> V) -> ((K) -> V)

    Memoize a function with a single cache slot, taking one argument.

    memo_once2

    fn[K1 : Eq, K2 : Eq, V] memo_once2(f : (K1, K2) -> V) -> ((K1, K2) -> V)

    Memoize a function with a single cache slot, taking two arguments.

    memo_once3

    fn[K1 : Eq, K2 : Eq, K3 : Eq, V] memo_once3(f : (K1, K2, K3) -> V) -> ((K1, K2, K3) -> V)

    Memoize a function with a single cache slot, taking three arguments.

    memo_once4

    fn[K1 : Eq, K2 : Eq, K3 : Eq, K4 : Eq, V] memo_once4(f : (K1, K2, K3, K4) -> V) -> ((K1, K2, K3, K4) -> V)

    Memoize a function with a single cache slot, taking four arguments.

    memo_once5

    fn[K1 : Eq, K2 : Eq, K3 : Eq, K4 : Eq, K5 : Eq, V] memo_once5(f : (K1, K2, K3, K4, K5) -> V) -> ((K1, K2, K3, K4, K5) -> V)

    Memoize a function with a single cache slot, taking five arguments.

    memoize1

    fn[K : Hash + Eq, V] memoize1(f : (K) -> V) -> ((K) -> V)

    memoize2

    fn[K1 : Hash + Eq, K2 : Hash + Eq, V] memoize2(f : (K1, K2) -> V) -> ((K1, K2) -> V)

    memoize3

    fn[K1 : Hash + Eq, K2 : Hash + Eq, K3 : Hash + Eq, V] memoize3(f : (K1, K2, K3) -> V) -> ((K1, K2, K3) -> V)

    memoize4

    fn[K1 : Hash + Eq, K2 : Hash + Eq, K3 : Hash + Eq, K4 : Hash + Eq, V] memoize4(f : (K1, K2, K3, K4) -> V) -> ((K1, K2, K3, K4) -> V)

    memoize5

    fn[K1 : Hash + Eq, K2 : Hash + Eq, K3 : Hash + Eq, K4 : Hash + Eq, K5 : Hash + Eq, V] memoize5(f : (K1, K2, K3, K4, K5) -> V) -> ((K1, K2, K3, K4, K5) -> V)

    normal_fonts

    let normal_fonts : String

    preset

    let preset : String

    common CSS resets for Respo pages

    render_node

    fn[T, U : Eq, G] render_node(mount_target :
    Node
    , store :
    Ref
    [U], renderer : () ->
    RespoNode
    [T, G] raise
    RespoCommonError
    , dispatch_action : (T) -> Unit raise
    RespoCommonError
    , _interval : Float?) -> Unit raise
    RespoCommonError

    Mount and start the render loop for a Respo application.

    Immutability contract

    store must hold an immutable value type (no mut struct fields). The render loop uses two optimizations to avoid unnecessary work:

    1. Physical equality (physical_equal): if store.val is the exact same heap object as in the previous frame, the render is skipped entirely.
    2. Logical equality (==): if store.val != prev_store but they compare equal, the render is also skipped.

    ⚠️ mut field pitfall: if your Store struct has mut fields and update() mutates them in place, then { ..store.val } only creates a shallow copy — but prev_store already sees the mutated values through the shared struct reference. Both checks then return true and the DOM is never updated.

    Correct pattern: update() must return a brand-new Store value.

    // WRONG — mut field + in-place mutation
    struct Store { mut count : Int } derive(Eq)
    fn Store::update(self : Store, op : Op) -> Op? {
    self.count 1 // mutates in place — prev_store sees this too!
    None
    }

    // CORRECT — no mut, update returns new store
    struct Store { count : Int } derive(Eq)
    fn Store::update(self : Store, op : Op) -> (Store, Op?) {
    ({ ..self, count: self.count + 1 }, None)
    }
    // caller:
    let (new_store, maybe_op) = app.store.val.update(op)
    app.store.val = new_store

    set_debug_mode

    fn set_debug_mode(enabled : Bool) -> Unit

    Enable or disable debug mode for Respo framework When enabled, logs state changes, events, and render cycles

    show_obscure_state

    fn show_obscure_state(msg : String, v : ObscureState) -> Unit

    try_load_storage

    fn[Model :
    FromJson
    + Default] try_load_storage(key : String) -> Model

    ui_button

    let ui_button : String

    ui_button_danger

    let ui_button_danger : String

    ui_button_primary

    let ui_button_primary : String

    ui_center

    let ui_center : String

    layout items in column and center them with flexbox demos https://ui.respo-mvc.org/#/layouts.html

    ui_column

    let ui_column : String

    layout items in column and center them with flexbox demos https://ui.respo-mvc.org/#/layouts.html

    ui_column_dispersive

    let ui_column_dispersive : String

    layout items in column with flexbox, space around demos https://ui.respo-mvc.org/#/layouts.html

    ui_column_evenly

    let ui_column_evenly : String

    layout items in column with flexbox, space evenly demos https://ui.respo-mvc.org/#/layouts.html

    ui_column_parted

    let ui_column_parted : String

    layout items in column with flexbox, space between demos https://ui.respo-mvc.org/#/layouts.html

    ui_expand

    let ui_expand : String

    expand item with flex:1

    ui_font_code

    let ui_font_code : String

    monospace font for code, Source Code Pro, Menlo, Ubuntu Mono, Consolas

    ui_font_fancy

    let ui_font_fancy : String

    fancy font for title, Josefin Sans, Helvetica neue, Arial refers to https://fonts.google.com/specimen/Josefin+Sans or https://cdn.tiye.me/favored-fonts/main-fonts.css

    ui_font_normal

    let ui_font_normal : String

    normal font for text, Hind, Helvatica, Arial refers to https://fonts.google.com/specimen/Hind or https://cdn.tiye.me/favored-fonts/main-fonts.css

    ui_fullscreen

    let ui_fullscreen : String

    full page with absolute position

    ui_global

    let ui_global : String

    ui_input

    let ui_input : String

    let ui_link : String

    ui_row

    let ui_row : String

    layout items in row with flexbox, items are stretched demos https://ui.respo-mvc.org/#/layouts.html

    ui_row_center

    let ui_row_center : String

    layout items in row with flexbox, center them demos https://ui.respo-mvc.org/#/layouts.html

    ui_row_dispersive

    let ui_row_dispersive : String

    layout items in row with flexbox, space around demos https://ui.respo-mvc.org/#/layouts.html

    ui_row_evenly

    let ui_row_evenly : String

    layout items in row with flexbox, space evenly demos https://ui.respo-mvc.org/#/layouts.html

    ui_row_middle

    let ui_row_middle : String

    layout items in row with flexbox, space between demos https://ui.respo-mvc.org/#/layouts.html

    ui_row_parted

    let ui_row_parted : String

    layout items in row with flexbox, space between demos https://ui.respo-mvc.org/#/layouts.html

    ui_textarea

    let ui_textarea : String