signals

    Fine-grained reactive signals for MoonBit (alien-signals inspired)

    signals
    reactive
    alien-signals
    ui
    vnode
    Download zip
    Author
    Version
    0.6.5
    License
    MIT
    Last updated
    2 months ago
    Downloads
    83K

    #mizchi/signals

    Fine-grained reactive signals library for MoonBit. Inspired by alien-signals and Solid.js.

    #What are Reactive Signals?

    Reactive signals provide a way to manage state that automatically tracks dependencies and propagates changes. When a signal's value changes, all computations and effects that depend on it are automatically updated.

    Key benefits:
    • Automatic dependency tracking: No need to manually specify what depends on what
    • Efficient updates: Only affected computations re-run when values change
    • Composable: Build complex reactive graphs from simple primitives

    Use cases:
    • UI frameworks: Automatically update views when data changes
    • State management: Manage application state with predictable updates
    • Data pipelines: Create derived values that stay in sync with source data
    • Game development: React to game state changes efficiently
    • Real-time systems: Propagate sensor/input changes through a system

    #Installation

    moon add mizchi/signals

    #API

    #Signal

    Holds a reactive value and notifies subscribers when it changes.

    let count = signal(0)

    // Get value (auto-tracked inside effects)
    count.get() // => 0

    // Set value
    count.set(5)

    // Update with function
    count.update(fn(n) { n + 1 })

    // Get without tracking (doesn't create dependency)
    count.peek()

    #memo / computed

    Creates a memoized value that recomputes only when dependencies change.

    let a = signal(2)
    let b = signal(3)

    let sum = memo(fn() { a.get() + b.get() })
    sum() // => 5

    a.set(10)
    sum() // => 13 (recomputed)
    sum() // => 13 (cached value)

    computed is an alias for memo.

    #render_effect

    Creates a side effect that re-runs synchronously when signals change.

    let count = signal(0)

    let dispose = render_effect(fn() {
    println("count = " + count.get().to_string())
    })

    count.set(1) // prints "count = 1"
    count.set(2) // prints "count = 2"

    dispose() // stop the effect

    #effect

    Similar to render_effect, but the initial execution is deferred via microtask queue (Solid.js createEffect style).

    let count = signal(0)
    let dispose = effect(fn() {
    println("deferred: " + count.get().to_string())
    })
    // Initial execution happens after current synchronous code completes

    Note: effect uses queue_microtask, which is environment-specific. See "Environment-specific async behavior" below.

    #batch

    Batches multiple updates so effects run only once.

    let a = signal(0)
    let b = signal(0)

    let _ = render_effect(fn() {
    println("sum = " + (a.get() + b.get()).to_string())
    })

    batch(fn() {
    a.set(1)
    b.set(2)
    })
    // Effect runs only once

    #on_cleanup

    Registers a cleanup function inside an effect. Called before the effect re-runs or when disposed.

    let _ = render_effect(fn() {
    let id = set_interval(...)
    on_cleanup(fn() {
    clear_interval(id)
    })
    })

    #create_root

    Creates a reactive scope root. Calling dispose stops all effects within.

    create_root(fn(dispose) {
    let _ = render_effect(fn() { ... })
    let _ = render_effect(fn() { ... })

    // Stop all effects at once
    dispose()
    })

    #untracked

    Runs a function with tracking disabled. Signal reads won't create dependencies.

    let _ = render_effect(fn() {
    // This signal read won't create a dependency
    untracked(fn() {
    let _ = some_signal.get()
    })
    })

    #Environment-specific async behavior

    This library uses queue_microtask for deferred effects (the effect function), but microtask scheduling is environment-specific.

    • JS target: Uses native queueMicrotask from browser/Node.js
    • Non-JS targets (wasm/native): Falls back to immediate execution

    For production use requiring deferred execution, consider implementing environment-appropriate async handling. If synchronous behavior is sufficient, use render_effect instead.

    #License

    MIT

    Snapshot

    pub trait Snapshot {
    fn snapshot(Self) -> Self
    }

    Trait for signal records that can produce a snapshot

    Computed

    pub(all) struct Computed[T] {
    node : ReactiveNode
    value : T?
    getter : () -> T
    }

    Computed node - derives value from dependencies (internal)

    Computed::get

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

    Get the computed value Automatically tracks dependency if called inside an effect/computed

    Computed::peek

    fn[T] Computed::peek(self : Computed[T]) -> T?

    Peek at current value without tracking

    Context

    pub struct Context[T] {
    id : Int
    default_value : () -> T
    providers : Array[(Int, () -> T)]
    }

    Context type with typed getter The current_getter is managed per-context and tracks the current value chain

    CoreSignal

    pub(all) struct CoreSignal[T] {
    node : ReactiveNode
    current_value : T
    pending_value : T
    }

    CoreSignal - internal reactive value container (use Signal for public API)

    CoreSignal::clear_subscribers

    fn[T] CoreSignal::clear_subscribers(self : CoreSignal[T]) -> Unit

    Clear all subscribers

    CoreSignal::get

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

    Get the current value of a core signal Automatically tracks dependency if called inside an effect/computed

    CoreSignal::peek

    fn[T] CoreSignal::peek(self : CoreSignal[T]) -> T

    Get value without tracking (peek) Returns the latest value (pending if dirty, otherwise current)

    CoreSignal::set

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

    Set a new value for the core signal Notifies all subscribers if value changed

    CoreSignal::subscriber_count

    fn[T] CoreSignal::subscriber_count(self : CoreSignal[T]) -> Int

    Get number of subscribers (for debugging)

    CoreSignal::update

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

    Update core signal value using a function

    EffectNode

    pub(all) struct EffectNode {
    node : ReactiveNode
    run_fn : () -> Unit
    }

    Effect node - runs side effects

    Flags

    pub(all) struct Flags {
    value : Int
    }

    Bitwise flag set

    Flags::get_value

    fn Flags::get_value(self : Flags) -> Int

    Flags::has

    fn Flags::has(self : Flags, flag : ReactiveFlags) -> Bool

    Flags::has_any

    fn Flags::has_any(self : Flags, flags : Array[ReactiveFlags]) -> Bool

    Flags::new

    fn Flags::new(initial : Int) -> Flags

    Flags::set

    fn Flags::set(self : Flags, flag : ReactiveFlags) -> Unit

    Flags::set_value

    fn Flags::set_value(self : Flags, v : Int) -> Unit

    Flags::unset

    fn Flags::unset(self : Flags, flag : ReactiveFlags) -> Unit

    Lens

    pub struct Lens[S, A] {
    get : (S) -> A
    set : (S, A) -> S
    }

    A lens focuses on a specific part of a larger structure

    Lens::new

    fn[S, A] Lens::new(get : (S) -> A, set : (S, A) -> S) -> Lens[S, A]

    LensStore

    pub struct LensStore[T] {
    source : Signal[T]
    }

    A store with lens-based field access Each focused lens creates a derived signal that only updates when that field changes

    LensStore::focus

    fn[T, A : Eq] LensStore::focus(self : LensStore[T], lens : Lens[T, A]) -> (Signal[A], (A) -> Unit)

    Focus on a field using a lens, returns getter and setter functions The getter is memoized and only recomputes when the focused value changes Uses render_effect for synchronous updates

    LensStore::new

    fn[T] LensStore::new(initial : T) -> LensStore[T]

    LensStore::snapshot

    fn[T] LensStore::snapshot(self : LensStore[T]) -> T

    Get the current snapshot of the entire store

    LensStore::update

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

    Update the entire store
    pub(all) struct Link {
    version : Int
    dep : ReactiveNode
    sub : ReactiveNode
    prev_sub : Link?
    next_sub : Link?
    prev_dep : Link?
    next_dep : Link?
    }

    Double-linked list node connecting dependencies and subscribers

    Owner

    pub struct Owner {
    id : Int
    parent : Owner?
    children : Array[Owner]
    cleanups : Array[() -> Unit]
    disposers : Array[() -> Unit]
    disposed : Bool
    }

    Owner - manages lifecycle of reactive computations (Solid.js style)

    Owner::dispose

    fn Owner::dispose(self : Owner) -> Unit

    Dispose this owner and all its children

    Owner::new

    fn Owner::new(parent : Owner?) -> Owner

    Create a new Owner

    ReactiveFlags

    pub(all) enum ReactiveFlags {
    None
    Mutable
    Watching
    RecursedCheck
    Recursed
    Dirty
    Pending
    }

    Reactive flags for tracking node state

    ReactiveFlags::to_int

    fn ReactiveFlags::to_int(self : ReactiveFlags) -> Int

    Convert flags to int for bitwise operations

    ReactiveNode

    pub(all) struct ReactiveNode {
    deps : Link?
    deps_tail : Link?
    subs : Link?
    subs_tail : Link?
    flags : Flags
    last_modified_cycle : Int
    effect_callback : () -> Unit?
    }

    Base reactive node - can be signal, computed, or effect

    ReactiveNode::new

    fn ReactiveNode::new(flags : Int) -> ReactiveNode

    Signal

    pub struct Signal[T] {
    inner : CoreSignal[T]
    }

    Signal type - holds a value and notifies subscribers on change. A reactive primitive that stores a value and automatically tracks dependencies.

    Signal::clear_subscribers

    fn[T] Signal::clear_subscribers(self : Signal[T]) -> Unit

    Clear all subscribers (for cleanup)

    Signal::get

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

    Get the current value and track dependency if inside an effect. When called inside an effect or memo, automatically subscribes to changes.

    Signal::new

    fn[T] Signal::new(initial : T) -> Signal[T]

    Create a new signal with initial value.

    Signal::peek

    fn[T] Signal::peek(self : Signal[T]) -> T

    Get value without tracking (won't create dependency). Useful when you need to read a signal without subscribing to it.

    Signal::set

    fn[T] Signal::set(self : Signal[T], new_value : T) -> Unit

    Set a new value and notify all subscribers.

    Signal::subscriber_count

    fn[T] Signal::subscriber_count(self : Signal[T]) -> Int

    Get number of subscribers (for debugging)

    Signal::update

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

    Update the value using a function. Useful for updating based on current value.

    SplitStore2

    pub struct SplitStore2[A, B] {
    field1 : Signal[A]
    field2 : Signal[B]
    }

    A record where each field is an independent Signal This is the most performant approach - updates only affect specific fields

    SplitStore2::new

    fn[A, B] SplitStore2::new(v1 : A, v2 : B) -> SplitStore2[A, B]

    SplitStore3

    pub struct SplitStore3[A, B, C] {
    field1 : Signal[A]
    field2 : Signal[B]
    field3 : Signal[C]
    }

    3-field version

    SplitStore3::new

    fn[A, B, C] SplitStore3::new(v1 : A, v2 : B, v3 : C) -> SplitStore3[A, B, C]

    all

    fn all(signals : Array[Signal[Bool]]) -> (() -> Bool)

    Create a signal that is true when all input signals are true.

    any

    fn any(signals : Array[Signal[Bool]]) -> (() -> Bool)

    Create a signal that is true when any input signal is true.

    batch

    fn[T] batch(f : () -> T) -> T

    Run a function in a batch - all signal updates are batched. Effects only run once after all updates complete.

    batch_end

    fn batch_end() -> Unit

    End a batch update - run all pending effects

    batch_start

    fn batch_start() -> Unit

    Start a batch update - effects won't run until batch ends

    check_dirty

    fn check_dirty(start_link : Link, sub : ReactiveNode, update_fn : (ReactiveNode) -> Bool) -> Bool

    Check if any dependency is dirty and needs update

    combine2

    fn[A, B, R] combine2(a : Signal[A], b : Signal[B], f : (A, B) -> R) -> (() -> R)

    Combine two signals into one. The getter recomputes when either signal changes.

    combine3

    fn[A, B, C, R] combine3(a : Signal[A], b : Signal[B], c : Signal[C], f : (A, B, C) -> R) -> (() -> R)

    Combine three signals into one

    combine4

    fn[A, B, C, D, R] combine4(a : Signal[A], b : Signal[B], c : Signal[C], d : Signal[D], f : (A, B, C, D) -> R) -> (() -> R)

    Combine four signals into one

    computed

    fn[T] computed(compute : () -> T) -> (() -> T)

    Create a computed signal (alias for memo with Signal-like API). Same as memo, but named to be more familiar for those coming from other reactive frameworks.

    core_signal

    fn[T] core_signal(initial : T) -> CoreSignal[T]

    Create a new core signal with initial value

    create_context

    fn[T] create_context(default_value : T) -> Context[T]

    Create a new context with a default value Similar to Solid.js createContext

    create_root

    fn[T] create_root(f : (() -> Unit) -> T) -> T

    Create a new reactive root scope. The function receives a dispose callback that cleans up all effects. Returns the result of the function.

    create_root_with_dispose

    fn[T] create_root_with_dispose(f : () -> T) -> (T, () -> Unit)

    Create a reactive root and return both result and dispose function

    effect

    fn effect(fn_ : () -> Unit) -> (() -> Unit)

    Create an effect that is deferred until after rendering completes. (Solid.js style createEffect - deferred execution via microtask) Returns a dispose function to stop the effect. Unlike render_effect, this runs asynchronously via microtask queue.

    effect_once

    fn effect_once(fn_ : () -> Unit) -> Unit

    Create a one-time effect that disposes itself after first run. Useful for initialization logic that should only run once.

    effect_when

    fn effect_when(condition : () -> Bool, fn_ : () -> Unit) -> (() -> Unit)

    Create an effect that only runs when condition is true. Uses render_effect for synchronous execution.

    end_batch

    fn end_batch() -> Unit

    End a batch update

    flatten

    fn[T] flatten(outer : Signal[Signal[T]]) -> (() -> T)

    Flatten a signal of signals. Unwraps nested Signal[Signal[T]] to a getter for T.

    flush

    fn flush() -> Unit

    Flush the effect queue

    get_active_sub

    fn get_active_sub() -> ReactiveNode?

    Get current active subscriber

    get_batch_depth

    fn get_batch_depth() -> Int

    Get current batch depth

    get_cycle

    fn get_cycle() -> Int

    Get current cycle

    get_owner

    fn get_owner() -> Owner?

    Get the current owner (if any)

    has_owner

    fn has_owner() -> Bool

    Check if currently inside an owner scope

    inc_cycle

    fn inc_cycle() -> Unit

    Increment cycle

    is_batching

    fn is_batching() -> Bool

    Check if we're currently inside a batch
    fn link(dep : ReactiveNode, sub : ReactiveNode, version : Int) -> Unit

    Create a link between dependency and subscriber

    memo

    fn[T] memo(compute : () -> T) -> (() -> T)

    Create a memoized computation. Returns a getter function that caches the result and recomputes only when dependencies change.

    new_effect_id

    fn new_effect_id() -> Int

    Generate a new unique effect ID
    fn[T] on(sig : Signal[T], callback : (T) -> Unit) -> (() -> Unit)

    Explicitly subscribe to a signal with a callback. Returns an unsubscribe function. Unlike effect(), this doesn't auto-track and only listens to this one signal. Callback is NOT called on initial subscription, only on subsequent changes.

    on_cleanup

    fn on_cleanup(cleanup : () -> Unit) -> Unit

    Register a cleanup function to run when the current scope disposes. Works in:
    • Effects: cleanup runs before effect re-runs or when effect disposes
    • Components: cleanup runs when component unmounts (Solid.js style - can be called directly in component body)

    on_immediate

    fn[T] on_immediate(sig : Signal[T], callback : (T) -> Unit) -> (() -> Unit)

    Subscribe and run immediately with current value. Like on, but also invokes the callback with the current value right away.

    on_mount

    fn on_mount(fn_ : () -> Unit) -> Unit

    Run a function once after mount (Solid.js style onMount). The function runs without tracking dependencies. Cleanup registered via on_cleanup inside will run when owner is disposed.
    fn[T] previous(sig : Signal[T]) -> (() -> T?)

    Create a signal that holds the previous value of another signal. Returns a getter function for the previous value (None on first read).

    previous_with_initial

    fn[T] previous_with_initial(sig : Signal[T], initial : T) -> (() -> T)

    Create a signal that holds the previous value with initial value. Unlike previous, this returns a plain T instead of T?.

    propagate

    fn propagate(start_link : Link) -> Unit

    Propagate changes through the dependency graph (simplified version)

    provide

    fn[T, R] provide(ctx : Context[T], value : T, f : () -> R) -> R

    Provide a context value for the current Owner and its descendants The value is associated with the current Owner and will be available to all effects and components created within this scope.

    purge_deps

    fn purge_deps(sub : ReactiveNode) -> Unit

    Purge unused deps after deps_tail

    queue_effect

    fn queue_effect(eff : EffectNode) -> Unit

    Queue an effect for execution

    register_disposer

    fn register_disposer(disposer : () -> Unit) -> Unit

    Register a disposer with the current owner Called internally by effect() to register its dispose function

    register_owner_cleanup

    fn register_owner_cleanup(cleanup : () -> Unit) -> Unit

    Register a cleanup with the current owner (alternative to onCleanup in effect)

    render_effect

    fn render_effect(fn_ : () -> Unit) -> (() -> Unit)

    Create an effect that runs immediately and re-runs when dependencies change. (Solid.js style createRenderEffect - synchronous execution) Returns a dispose function to stop the effect. Supports on_cleanup() calls inside the effect.

    run_with_cleanup_tracking

    fn[T] run_with_cleanup_tracking(cleanups : Array[() -> Unit], f : () -> T) -> T

    Run a function with cleanup tracking enabled

    run_with_owner

    fn[T] run_with_owner(owner : Owner, f : () -> T) -> T

    Run a function with a specific owner as current

    select

    fn[T] select(items : Signal[Array[T]], index : Signal[Int]) -> (() -> T?)

    Select from an array signal by index signal. Returns None if index is out of bounds.

    set_active_sub

    fn set_active_sub(sub : ReactiveNode?) -> ReactiveNode?

    Set active subscriber, returns previous

    set_current_cleanups

    fn set_current_cleanups(cleanups : Array[() -> Unit]?) -> Array[() -> Unit]?

    Set the current cleanup array (used internally by effect)

    shallow_propagate

    fn shallow_propagate(start_link : Link) -> Unit

    Shallow propagate - mark direct subscribers as dirty

    sig_filter

    fn[T] sig_filter(sig : Signal[T], predicate : (T) -> Bool) -> Signal[T?]

    Filter signal updates - only updates when predicate is true. Returns a signal that only updates when predicate passes.

    sig_filter_map

    fn[T, U] sig_filter_map(sig : Signal[T], f : (T) -> U?) -> Signal[U?]

    Filter and map signal updates Uses render_effect for synchronous execution

    sig_map

    fn[T, U] sig_map(sig : Signal[T], f : (T) -> U) -> (() -> U)

    Map a signal's value through a function. Returns a getter function (like memo) that recomputes when the signal changes.

    signal

    fn[T] signal(initial : T) -> Signal[T]

    Convenience function to create a signal. Shorthand for Signal::new.

    start_batch

    fn start_batch() -> Unit

    Start a batch update

    switch_

    fn[T] switch_(condition : Signal[Bool], on_true : Signal[T], on_false : Signal[T]) -> (() -> T)

    Switch between signals based on a boolean signal. Returns value from on_true when condition is true, otherwise on_false.

    to_getter

    fn[T] to_getter(sig : Signal[T]) -> (() -> T)

    Create a read-only getter for a signal
    fn unlink(lnk : Link, sub : ReactiveNode) -> Link?

    Unlink a dependency link, returns next dep link

    untracked

    fn[T] untracked(f : () -> T) -> T

    Run a function without tracking (useful for avoiding circular deps). Signal reads inside this function won't create subscriptions.

    use_context

    fn[T] use_context(ctx : Context[T]) -> T

    Use a context value - returns the current provided value or default Walks up the Owner chain to find the nearest provided value. Similar to Solid.js useContext

    watch

    fn[T : Eq] watch(source : () -> T, callback : (T, T) -> Unit) -> (() -> Unit)

    Watch a computed expression and run callback when it changes. Returns a dispose function. The callback receives (newValue, oldValue).

    watch_immediate

    fn[T : Eq] watch_immediate(source : () -> T, callback : (T, T?) -> Unit) -> (() -> Unit)

    Watch with immediate execution (runs callback on first value too). The callback receives (newValue, oldValue?) where oldValue is None on first run.

    with_parent_owner

    fn[T] with_parent_owner(captured_owner : Owner?, f : () -> T) -> T

    Run a function with a captured parent owner context (if any) This is useful for rendering child components that need to inherit the parent's owner. If captured_owner is Some, runs with that owner as context. If captured_owner is None, runs directly without owner context.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io