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
last month
Downloads
81K

#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