Skip to content

Reactivity

wybthon.reactivity

reactivity

Signal-based reactive primitives with an ownership tree.

This module is the heart of Wybthon's SolidJS-inspired reactivity. It implements a synchronous, glitch-free, pull-based reactive graph that matches SolidJS's observable semantics:

  • Writing a signal synchronously propagates to dependents before the setter returns (outside an explicit :func:batch).
  • Memos are lazy: they recompute only when read after a dependency changed, and they skip notifying their own observers when the recomputed value is unchanged (the glitch-free "equality short-circuit").
  • Effects run in a second phase after pure computations have settled, so an effect never observes a half-updated graph.

Every reactive computation (effect, memo) is also owned by a parent scope. When that scope re-runs or is disposed, all child computations are torn down automatically, preventing leaks and giving lifecycle semantics that match component mount/unmount boundaries.

Core types:

  • Signal: mutable reactive container.
  • Owner: base ownership scope (cleanups + children).
  • Computation: a reactive computation (effect or memo) that is itself an ownership scope.
  • Resource: async data wrapper with data/error/loading.

Public primitives:

Example

A counter with a derived doubled value::

from wybthon import create_signal, create_memo, create_effect

count, set_count = create_signal(0)
doubled = create_memo(lambda: count() * 2)
create_effect(lambda: print("doubled:", doubled()))

set_count(2)  # synchronously prints "doubled: 4"

Classes:

Name Description
Owner

Base reactive ownership scope.

Computation

Reactive computation that tracks sources and re-runs when they change.

Signal

Mutable reactive container that notifies observers on change.

ReactiveProps

Reactive proxy over a component's props dict.

Resource

Async data accessor with reactive loading/error state.

Functions:

Name Description
batch

Batch signal updates so dependent effects flush once at the end.

untrack

Run fn without tracking any signal reads.

create_resource

Create an async Resource accessor.

get_owner

Return the current reactive owner scope, if any.

run_with_owner

Run fn under the given ownership scope.

create_signal

Create a reactive signal and return (getter, setter).

create_effect

Create an auto-tracking reactive effect.

create_render_effect

Create an effect that runs in the render phase, before user effects.

create_computed

Create an eagerly-run computation in the pure (pre-render) phase.

create_reaction

Create an on-change reaction with manual tracking.

create_unique_id

Return a process-unique id string (e.g., for for/id attribute pairs).

catch_error

Run fn with an error handler, catching errors thrown now or later.

on_error

Register an error handler on the current reactive owner scope.

create_deferred

Return a getter that trails source, updating on the next event-loop tick.

create_memo

Create an auto-tracking computed value and return its getter.

on_mount

Register a callback to run once after the component mounts.

on_cleanup

Register a cleanup callback on the active reactive owner.

children

Resolve and memoize reactive children, returning a memo getter.

get_props

Return the ReactiveProps proxy for the current component.

on

Create an effect with explicit dependencies.

create_root

Run fn inside an independent reactive root.

merge_props

Merge multiple prop sources into a reactive proxy.

split_props

Split a props source into groups by key name, plus a rest group.

map_array

Map a reactive list with stable per-item scopes (keyed by identity).

index_array

Map a reactive list with stable per-index scopes.

create_selector

Create an O(1) selection signal.

Owner

Owner()

Base reactive ownership scope.

Tracks child owners and cleanup callbacks. When disposed, children are disposed first (depth-first), then this owner's own cleanups run. This mirrors SolidJS's ownership model.

Attributes:

Name Type Description
_parent Optional[Owner]

Parent Owner, or None for roots.

_children Optional[Dict[int, Owner]]

Child owners, keyed by id(child) (insertion-ordered). Allocated lazily on the first child.

_cleanups Optional[List[Callable[[], Any]]]

Callbacks invoked LIFO during disposal. Allocated lazily on the first registration.

_disposed bool

True after dispose() has run; further calls are no-ops.

_context_map Optional[Dict[Any, Any]]

Lazily-allocated dict of context values stored at this owner (used by Provider).

_error_handler Optional[Callable[..., Any]]

Optional callback receiving exceptions raised by descendant computations (installed by ErrorBoundary and catch_error).

Methods:

Name Description
dispose

Tear down this owner and all descendants.

dispose

dispose() -> None

Tear down this owner and all descendants.

Disposes children depth-first, then runs this owner's cleanup callbacks in LIFO order, and finally detaches from its parent. Subsequent calls are no-ops.

Computation

Computation(fn: Callable[[], Any], *, is_effect: bool = False, is_render: bool = False, is_pure: bool = False, is_memo: bool = False, value: Any = None, equals: Any = _DEFAULT_EQUALS)

Bases: Owner

Reactive computation that tracks sources and re-runs when they change.

A computation is either an effect (run for its side effects) or a memo (a lazily-recomputed derived value that other computations can observe). It is also an ownership scope: child computations created during execution are disposed before each re-run, preventing leaks from conditionally-created effects.

Attributes:

Name Type Description
_fn

The callback executed by _update().

_sources Optional[Dict[Any, None]]

Sources (signals or memos) read during the last run. An insertion-ordered dict used as an ordered set, allocated lazily on the first tracked read.

_state int

One of _CLEAN / _CHECK / _DIRTY.

_is_effect

True for effects (scheduled in the effects phase).

_is_memo

True for memos (carry a value and observers).

Methods:

Name Description
dispose

Dispose the computation and unsubscribe from all dependencies.

dispose

dispose() -> None

Dispose the computation and unsubscribe from all dependencies.

Removes dependency edges, drops this node as a source for any of its observers, tears down child owners, and runs cleanups.

Signal

Signal(value: T, *, equals: Any = _DEFAULT_EQUALS)

Bases: Generic[T]

Mutable reactive container that notifies observers on change.

Most code uses create_signal which returns a (getter, setter) tuple instead of exposing Signal instances directly. This class is part of the public surface so Signal[T] can be used in type hints.

Parameters:

Name Type Description Default
value T

The initial value.

required
equals Any

Equality policy. See create_signal for the full semantics.

_DEFAULT_EQUALS

Methods:

Name Description
get

Return the current value and subscribe the active computation.

peek

Return the current value without subscribing the active computation.

set

Write a new value and synchronously notify observers if it changed.

get

get() -> T

Return the current value and subscribe the active computation.

When called inside an effect, memo, or reactive hole, this signal is added to that computation's dependency set so it re-runs on future writes. Outside a tracking context the read is untracked.

Returns:

Type Description
T

The current value held by the signal.

peek

peek() -> T

Return the current value without subscribing the active computation.

set

set(value: T) -> None

Write a new value and synchronously notify observers if it changed.

Equality is determined by the equals policy passed to the constructor (default: is then ==, with equals=False to bypass the check entirely). Outside a batch, dependent memos become readable immediately and effects run before this call returns.

Parameters:

Name Type Description Default
value T

The new value to store.

required

ReactiveProps

ReactiveProps(props: dict, defaults: Optional[Dict[str, Any]] = None)

Reactive proxy over a component's props dict.

Every attribute or item access returns a reactive accessor: a zero-arg callable that returns the current value and tracks the read. This mirrors SolidJS's props.x semantics, adapted to Python.

Access patterns:

Expression Returns Notes
props.name callable getter Stable across reads.
props.name() current value Tracked when called inside an effect or hole.
props["name"] callable getter Same as props.name.
props.get("name", default) callable getter Returns default when missing.
props.value("name", default) current value One-shot snapshot, with auto-unwrap.

Embed the accessor directly in the VNode tree to create an automatic reactive hole; the surrounding DOM region updates only when the prop changes.

Example
@component
def Greeting():
    props = get_props()
    # Auto-hole: only the text node updates when ``name`` changes.
    return p("Hello, ", props.name, "!")
Note

ReactiveProps is read-only. Parents and the reconciler update it via the internal _update method.

Methods:

Name Description
value

Return the current value for key (tracked, with auto-unwrap).

get

Return a callable getter for key, falling back to default if missing.

keys

Return the prop names currently set on this instance.

values

Return current prop values as a list (tracked).

items

Return (key, current_value) pairs (tracked).

value

value(key: str, default: Any = _MISSING) -> Any

Return the current value for key (tracked, with auto-unwrap).

If the stored prop value is a getter, it's invoked and the result returned, mirroring _make_getter. If default is provided, it's returned when key is absent from both the props dict and the component's parameter defaults.

Parameters:

Name Type Description Default
key str

Prop name.

required
default Any

Value returned when the key is missing. When omitted, missing keys yield None.

_MISSING

Returns:

Type Description
Any

The current prop value (auto-unwrapped if it's a getter).

get

get(key: str, default: Any = None) -> Callable[[], Any]

Return a callable getter for key, falling back to default if missing.

When key exists on the prop bag (in raw props, prior signals, or component-parameter defaults), the returned getter reads the underlying signal: a tracking scope subscribes to it.

When key is missing, the getter returns default on each call without creating a tracked signal. This means repeated get(key, x) / get(key, y) calls each return their own default (no sticky behavior). Components that need reactivity for a possibly-missing prop should declare it as a parameter with a default value; @component ensures a signal is created up front so future updates always propagate.

Parameters:

Name Type Description Default
key str

Prop name.

required
default Any

Value returned by the fallback getter when key is missing.

None

Returns:

Type Description
Callable[[], Any]

A zero-arg callable. Subscribers to a missing-key getter are

Callable[[], Any]

not notified when the prop later appears.

keys

keys() -> Any

Return the prop names currently set on this instance.

The result reflects the latest props pushed into the proxy (it's not tracked as a reactive read).

values

values() -> Any

Return current prop values as a list (tracked).

items

items() -> Any

Return (key, current_value) pairs (tracked).

Resource

Resource(fetcher: FetchFn, source: Optional[Callable[[], Any]] = None, *, initial_value: Any = _MISSING)

Bases: Generic[R]

Async data accessor with reactive loading/error state.

Matches SolidJS's resource shape: the resource is the accessor. Call it to read the current data (tracked); read loading, error, latest, and state as properties (also tracked).

While a resource is in its initial "pending" state, reading it under a Suspense boundary automatically registers it with that boundary; no manual wiring is needed. Refetches ("refreshing" state) don't re-trigger Suspense; the previous data stays available, matching SolidJS.

States:

  • "unresolved": never fetched (source was None/False).
  • "pending": first fetch in flight, no data yet.
  • "ready": data available.
  • "refreshing": refetch in flight, previous data still readable.
  • "errored": last fetch raised.
Example
async def load_user(user_id, signal=None):
    resp = await fetch(f"/api/users/{user_id}")
    return await resp.json()

user = create_resource(user_id, load_user)
p("Name: ", span(lambda: (user() or {}).get("name", "...")))

Methods:

Name Description
mutate

Set the resource's data directly, without fetching.

refetch

Cancel any in-flight request and start a new fetch.

cancel

Abort the current in-flight fetch, if any.

Attributes:

Name Type Description
loading bool

True while a fetch is in flight (tracked read).

error Optional[Any]

The most recent exception, or None (tracked read).

latest Optional[R]

The most recent data, even while refreshing (tracked read).

state str

The current lifecycle state string (tracked read).

loading property

loading: bool

True while a fetch is in flight (tracked read).

error property

error: Optional[Any]

The most recent exception, or None (tracked read).

latest property

latest: Optional[R]

The most recent data, even while refreshing (tracked read).

Unlike calling the resource, reading latest never registers with a Suspense boundary.

state property

state: str

The current lifecycle state string (tracked read).

mutate

mutate(value: Union[R, Callable[[Optional[R]], R]]) -> Optional[R]

Set the resource's data directly, without fetching.

Supports functional updates like a signal setter. Clears any error and marks the resource "ready".

Parameters:

Name Type Description Default
value Union[R, Callable[[Optional[R]], R]]

The new data, or a callable receiving the current data.

required

Returns:

Type Description
Optional[R]

The stored value.

refetch

refetch() -> None

Cancel any in-flight request and start a new fetch.

The resource enters "pending" (first fetch) or "refreshing" (data already present). Older in-flight tasks are ignored when they resolve.

cancel

cancel() -> None

Abort the current in-flight fetch, if any.

Calls AbortController.abort() on the wrapped browser controller, cancels the asyncio task, and resets loading to False without touching the data or error state.

signal

signal(value: T) -> Signal[T]

Create a new Signal with the given initial value.

Most code should call create_signal instead; this function exists for low-level uses that need the raw Signal object (for example, attaching custom subscribers).

Parameters:

Name Type Description Default
value T

The initial value stored in the signal.

required

Returns:

Type Description
Signal[T]

A new Signal[T] with default equality semantics.

computed

computed(fn: Callable[[], T]) -> _Computed[T]

Create a _Computed value derived from other signals.

Low-level helper used by create_memo. Most code should use create_memo directly because it returns a plain getter (matching SolidJS's API).

Parameters:

Name Type Description Default
fn Callable[[], T]

Zero-arg callable. Re-evaluated when any signal it reads changes.

required

Returns:

Type Description
_Computed[T]

A _Computed[T] instance whose .get() returns the current value.

effect

effect(fn: Callable[[], Any]) -> Computation

Run a reactive effect immediately and return its Computation.

Low-level helper. Most code should use create_effect which additionally supports receiving the previous return value as an argument.

Parameters:

Name Type Description Default
fn Callable[[], Any]

Zero-arg callback. Re-runs when any signal it reads changes.

required

Returns:

Type Description
Computation

The underlying Computation. Call .dispose() to stop the effect.

on_effect_cleanup

on_effect_cleanup(comp: Computation, fn: Callable[[], Any]) -> None

Register fn to run when comp is disposed.

Parameters:

Name Type Description Default
comp Computation

The computation whose disposal should trigger the cleanup.

required
fn Callable[[], Any]

Zero-arg cleanup callback.

required

batch

batch(fn: Optional[Callable[[], T]] = None) -> Union[T, _Batch]

Batch signal updates so dependent effects flush once at the end.

Two call shapes are supported:

  1. Context manager (Pythonic):
    with batch():
        set_a(1)
        set_b(2)
    
  2. Callback (SolidJS style):
    batch(lambda: (set_a(1), set_b(2)))
    

When called with a function, the function's return value is returned. Effects are flushed synchronously when the outermost batch exits, matching SolidJS semantics.

Parameters:

Name Type Description Default
fn Optional[Callable[[], T]]

Optional zero-arg callable. When omitted, returns a context manager.

None

Returns:

Type Description
Union[T, _Batch]

Either a _Batch context manager (when fn is None) or the

Union[T, _Batch]

return value of fn.

untrack

untrack(fn: Callable[[], T]) -> T

Run fn without tracking any signal reads.

Useful inside effects when you need to read a signal without creating a dependency, or during component setup to seed local state from a prop without subscribing.

Inside untrack the dev-mode destructured-prop warning is also silenced, so count, set_count = create_signal(untrack(initial)) cleanly opts out of the noise.

Parameters:

Name Type Description Default
fn Callable[[], T]

Zero-arg callable to invoke with tracking suppressed.

required

Returns:

Type Description
T

Whatever fn returns.

Example
create_effect(lambda: print("a changed:", a(), "b is:", untrack(b)))

create_resource

create_resource(source_or_fetcher: Union[Callable[[], Any], Callable[..., Awaitable[R]]], fetcher: Optional[Callable[..., Awaitable[R]]] = None, *, initial_value: Any = _MISSING) -> Resource[R]

Create an async Resource accessor.

Can be called two ways:

  • create_resource(fetcher): simple fetcher, fetches immediately.
  • create_resource(source, fetcher): fetches whenever the source getter's tracked value changes; when the source yields None or False the fetch is skipped (the resource stays unresolved).

The fetcher may be sync or async. When it declares a positional parameter, the current source value is passed as the first argument. When it accepts a signal keyword argument, an AbortSignal is passed for cancellation support in the browser.

Parameters:

Name Type Description Default
source_or_fetcher Union[Callable[[], Any], Callable[..., Awaitable[R]]]

When called with one argument, this is the fetcher. When called with two, this is the source getter (typically a signal accessor).

required
fetcher Optional[Callable[..., Awaitable[R]]]

Optional fetcher. Required when source_or_fetcher is a source getter.

None
initial_value Any

Optional seed data. When provided, the resource starts "ready" with this value, so reading it never triggers a Suspense fallback; the first fetch runs as a "refreshing" state instead of "pending", matching SolidJS's initialValue option.

_MISSING

Returns:

Type Description
Resource[R]

A Resource[R]. Call it to read the data; read .loading,

Resource[R]

.error, .latest, and .state for the surrounding UI state.

Example
user_id, set_user_id = create_signal(1)

async def load_user(uid, signal=None):
    resp = await fetch(f"/api/users/{uid}")
    return await resp.json()

user = create_resource(user_id, load_user)
cached = create_resource(load_all, initial_value=[])

get_owner

get_owner() -> Optional[Owner]

Return the current reactive owner scope, if any.

Capture the owner before crossing async boundaries (e.g., await) and restore it with run_with_owner to maintain proper lifecycle management.

Returns:

Type Description
Optional[Owner]

The active Owner, or None when called outside any reactive

Optional[Owner]

scope.

run_with_owner

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

Run fn under the given ownership scope.

Useful for restoring context after an await boundary where the reactive owner would otherwise be lost.

Parameters:

Name Type Description Default
owner Optional[Owner]

The owner to install for the duration of fn. Pass None to disable ownership tracking entirely.

required
fn Callable[[], T]

Zero-arg callable to execute.

required

Returns:

Type Description
T

Whatever fn returns.

Example
async def load():
    owner = get_owner()
    data = await fetch_something()
    run_with_owner(owner, lambda: create_effect(lambda: use(data)))

create_signal

create_signal(value: T, *, equals: Any = _DEFAULT_EQUALS) -> tuple

Create a reactive signal and return (getter, setter).

Works inside or outside components. Inside a stateful component the signal is captured by the render function's closure and persists naturally; there's no cursor system or "rules of hooks".

The setter supports functional updates, matching SolidJS: when called with a callable, the callable receives the current value and its return value becomes the new value. To store a callable as the signal's value, wrap it: set_fn(lambda _prev: my_callable).

Parameters:

Name Type Description Default
value T

Initial value stored in the signal.

required
equals Any

Equality policy controlling when subscribers are notified. Accepts:

  • The default sentinel (or True): value equality (new == old) with an identity fast-path. Skips notification when the new value is is-identical or ==-equal to the old. This matches Python's natural intuition; re-setting an unchanged value is a no-op, and a fresh container with equal contents also skips notification.
  • False: always notify on every set() call, even when the new value is identical or equal to the old. Useful for "fire a change event" signals.
  • A callable (old, new) -> bool: skip notification when it returns True. Pass equals=lambda a, b: a is b for SolidJS-style identity-only semantics.
_DEFAULT_EQUALS

Returns:

Type Description
tuple

A (getter, setter) tuple. The getter is a zero-arg callable

tuple

suitable for embedding as a reactive hole; its .peek() method

tuple

returns the current value without subscribing the caller. The

tuple

setter returns the value that was stored.

Example
count, set_count = create_signal(0)
print(count())              # 0
set_count(5)
print(count())              # 5
set_count(lambda c: c + 1)  # functional update
print(count())              # 6
count.peek()                # 6, without subscribing

create_effect

create_effect(fn: Callable[..., Any]) -> Computation

Create an auto-tracking reactive effect.

The effect runs immediately and re-runs whenever any signal read inside fn changes. on_cleanup may be called inside fn to register per-run cleanup that runs before re-execution and on disposal.

If fn accepts a positional parameter, the previous return value is passed on each re-execution (None on the first run), matching SolidJS's createEffect(prev => ...).

Inside a component, the effect is automatically disposed on unmount.

Parameters:

Name Type Description Default
fn Callable[..., Any]

Zero- or one-arg callable. When it accepts an argument, the previous return value is forwarded.

required

Returns:

Type Description
Computation

The underlying Computation. Call .dispose() to stop the effect

Computation

manually.

Example
count, set_count = create_signal(0)
create_effect(lambda prev: (print("was", prev), count())[1])

create_render_effect

create_render_effect(fn: Callable[..., Any]) -> Computation

Create an effect that runs in the render phase, before user effects.

Matches SolidJS's createRenderEffect: within one flush, all pending render effects execute before any create_effect callbacks, and before the batched DOM ops are committed. Wybthon's internal reactive holes and prop bindings run in this phase, so a render effect observes the DOM in the same state the framework's own bindings do (updates emitted but not yet committed).

Parameters:

Name Type Description Default
fn Callable[..., Any]

Zero- or one-arg callable (the previous return value is forwarded when accepted, as with create_effect).

required

Returns:

Type Description
Computation

The underlying Computation.

create_computed

create_computed(fn: Callable[..., Any]) -> Computation

Create an eagerly-run computation in the pure (pre-render) phase.

Matches SolidJS's createComputed: within one flush, all pending create_computed computations run before any render effects, so signals they write are fully settled before the DOM updates. Prefer create_memo for plain derived values; reach for create_computed only when you must push a value into another signal.

Parameters:

Name Type Description Default
fn Callable[..., Any]

Zero- or one-arg callable.

required

Returns:

Type Description
Computation

The underlying Computation.

create_reaction

create_reaction(on_invalidate: Callable[[], Any]) -> Callable[[Callable[[], Any]], None]

Create an on-change reaction with manual tracking.

Matches SolidJS's createReaction: the returned track function runs its argument once with tracking enabled; the first time any tracked dependency changes, on_invalidate fires (untracked) and tracking stops until track is called again. Useful for "notify me once" patterns where the rendering of the change is decoupled from its detection.

Parameters:

Name Type Description Default
on_invalidate Callable[[], Any]

Zero-arg callback invoked once per tracked change.

required

Returns:

Type Description
Callable[[Callable[[], Any]], None]

A track(fn) function. Call it with a zero-arg callable whose

Callable[[Callable[[], Any]], None]

signal reads should be tracked.

Example
count, set_count = create_signal(0)
track = create_reaction(lambda: print("count changed!"))

track(count)     # start tracking
set_count(1)     # prints "count changed!"
set_count(2)     # nothing: tracking stopped
track(count)     # re-arm

create_unique_id

create_unique_id() -> str

Return a process-unique id string (e.g., for for/id attribute pairs).

Matches SolidJS's createUniqueId.

Returns:

Type Description
str

A short unique string like "wyb-7".

catch_error

catch_error(fn: Callable[[], T], handler: Callable[[Any], Any]) -> Optional[T]

Run fn with an error handler, catching errors thrown now or later.

Matches SolidJS's catchError. The handler receives exceptions raised synchronously by fn and exceptions raised later by any effect created inside fn (they route up the ownership tree to the nearest handler).

Parameters:

Name Type Description Default
fn Callable[[], T]

Zero-arg callable to execute under the error scope.

required
handler Callable[[Any], Any]

Callback receiving the exception.

required

Returns:

Type Description
Optional[T]

The return value of fn, or None when fn raised.

Example
result = catch_error(lambda: risky_setup(), lambda e: report(e))

on_error

on_error(handler: Callable[[Any], Any]) -> None

Register an error handler on the current reactive owner scope.

Matches SolidJS's onError: exceptions raised by child computations (effects created under this scope, reactive holes, descendant component setups) route to the nearest ancestor handler. Multiple calls in the same scope chain the handlers; each runs in registration order.

Unlike catch_error, which wraps a single function call in its own scope, on_error protects everything created under the current scope for its remaining lifetime.

Parameters:

Name Type Description Default
handler Callable[[Any], Any]

Callback receiving the exception.

required

Raises:

Type Description
RuntimeError

If called outside any reactive scope.

Example
@component
def Dashboard():
    on_error(lambda exc: report_to_sentry(exc))
    ...

create_deferred

create_deferred(source: Callable[[], T]) -> Callable[[], T]

Return a getter that trails source, updating on the next event-loop tick.

A lightweight analogue of SolidJS's createDeferred: reads of the returned getter don't recompute synchronously when source changes; instead, the new value is published asynchronously (via the running asyncio loop when one exists, else immediately). Use it to decouple expensive consumers from rapid-fire updates.

Parameters:

Name Type Description Default
source Callable[[], T]

Zero-arg tracked getter.

required

Returns:

Type Description
Callable[[], T]

A zero-arg getter for the deferred value.

create_memo

create_memo(fn: Callable[[], T], *, equals: Any = _DEFAULT_EQUALS) -> Callable[[], T]

Create an auto-tracking computed value and return its getter.

Re-computes lazily, only when read after a tracked source changed. Inside a component, the underlying computation is disposed on unmount.

Parameters:

Name Type Description Default
fn Callable[[], T]

Zero-arg callable producing the derived value.

required
equals Any

Equality policy controlling when the memo's own observers are notified after a recompute. Same semantics as create_signal: the default is value equality with an identity fast path, False always notifies, and a callable (old, new) -> bool skips notification when it returns True.

_DEFAULT_EQUALS

Returns:

Type Description
Callable[[], T]

A zero-arg callable. Reading it inside a tracking scope creates

Callable[[], T]

a dependency on the memoised value. Its .peek() method returns

Callable[[], T]

the (up-to-date) value without subscribing the caller.

Example
doubled = create_memo(lambda: count() * 2)
print(doubled())      # reactive read
print(doubled.peek())  # untracked read

# Notify only when the result list changes length:
heads = create_memo(lambda: items()[:5], equals=lambda a, b: len(a) == len(b))

on_mount

on_mount(fn: Callable[[], Any]) -> None

Register a callback to run once after the component mounts.

Must be called during a component's setup phase (the body of a @component function, before the return).

Parameters:

Name Type Description Default
fn Callable[[], Any]

Zero-arg callback invoked once after the first render commits.

required

Raises:

Type Description
RuntimeError

If called outside a component setup phase.

on_cleanup

on_cleanup(fn: Callable[[], Any]) -> None

Register a cleanup callback on the active reactive owner.

Lifecycle differs by call site:

  • Inside create_effect: runs before each re-execution and on final disposal.
  • Inside a component's setup phase: runs when the component unmounts.
  • Inside a reactive hole: runs before each hole re-evaluation and when the hole is disposed.

Parameters:

Name Type Description Default
fn Callable[[], Any]

Zero-arg cleanup callback.

required

Raises:

Type Description
RuntimeError

If called outside any reactive scope.

children

children(fn: Callable[[], Any]) -> Callable[[], List[Any]]

Resolve and memoize reactive children, returning a memo getter.

Wraps a getter that returns children (e.g., lambda: props.children()) and returns a memo that flattens nested lists and unwraps callables. Matches SolidJS's children() helper.

Parameters:

Name Type Description Default
fn Callable[[], Any]

Zero-arg getter that returns the raw children value (typically lambda: get_props().children()).

required

Returns:

Type Description
Callable[[], List[Any]]

A zero-arg memo getter producing a flat list of resolved children.

Example
@component
def Card(title=""):
    props = get_props()
    resolved = children(lambda: props.children())
    return section(h3(props.title), *resolved())

get_props

get_props() -> 'ReactiveProps'

Return the ReactiveProps proxy for the current component.

The returned object provides reactive access to individual props. Each attribute / index lookup yields a callable getter; call the getter to read the current value (tracked when called inside an effect or hole).

Most components should rely on the destructured parameters provided by @component. Use get_props() for advanced cases such as generic wrappers, key iteration, or proxy-mode interop.

Returns:

Type Description
'ReactiveProps'

The cached ReactiveProps proxy for this component instance.

Raises:

Type Description
RuntimeError

If called outside a component setup phase.

Example
@component
def Greet(name="world"):
    # ``name`` is already a reactive accessor.
    return p("Hello, ", name, "!")

@component
def Advanced():
    props = get_props()
    create_effect(lambda: print("name:", props.name()))

on

on(deps: Union[Callable[[], Any], List[Callable[[], Any]]], fn: Callable[..., Any], defer: bool = False) -> Computation

Create an effect with explicit dependencies.

deps may be a single getter or a list of getters. fn receives the current value(s) as positional arguments. Only the listed deps are tracked; the body of fn runs inside untrack.

Parameters:

Name Type Description Default
deps Union[Callable[[], Any], List[Callable[[], Any]]]

One getter or a list of getters to subscribe to.

required
fn Callable[..., Any]

Callback receiving the current dep value(s) on each change.

required
defer bool

When True, skip the first invocation (so fn runs only on subsequent changes, not the initial read).

False

Returns:

Type Description
Computation

The underlying Computation.

Example
on(count, lambda v: print("count is now", v))
on([a, b], lambda va, vb: print(f"a={va}, b={vb}"), defer=True)

create_root

create_root(fn: Callable[[Callable[[], None]], T]) -> T

Run fn inside an independent reactive root.

Useful for spawning long-lived reactive work that shouldn't be tied to the surrounding component's lifecycle (e.g., global stores).

Parameters:

Name Type Description Default
fn Callable[[Callable[[], None]], T]

Callable receiving a dispose callback. Calling dispose() tears down the root and any effects created inside it.

required

Returns:

Type Description
T

Whatever fn returns.

Example
result = create_root(lambda dispose: setup_global_state(dispose))

merge_props

merge_props(*sources: Any) -> '_MergedProps'

Merge multiple prop sources into a reactive proxy.

Each source may be a plain dict, a zero-arg getter that returns a dict, or another _MergedProps / _SplitProps. Later sources override earlier ones on key conflicts.

The returned object supports dict-like access ([], .get, in, iteration). When a source is a callable, it's called on each property access, so signal reads inside the getter are tracked by the current reactive computation.

Parameters:

Name Type Description Default
*sources Any

One or more prop sources, in priority order (rightmost wins).

()

Returns:

Type Description
'_MergedProps'

A reactive merged-props proxy.

Example
defaults = {"size": "md", "variant": "solid"}
final = merge_props(defaults, props)
# final["size"] lazily reads from props, then falls back to defaults

split_props

split_props(props: Any, *key_groups: List[str]) -> Tuple[Any, ...]

Split a props source into groups by key name, plus a rest group.

Parameters:

Name Type Description Default
props Any

A dict, callable getter, or _MergedProps / _SplitProps to split.

required
*key_groups List[str]

One or more lists of keys defining each group.

()

Returns:

Type Description
Any

A tuple (group1, group2, ..., rest) where each group is a

...

reactive proxy that lazily reads from the original props. The

Tuple[Any, ...]

final rest group contains every key not claimed by an earlier

Tuple[Any, ...]

group.

Example
local, rest = split_props(props, ["class", "style"])
# local["class"] lazily reads from props
# rest contains every other key

map_array

map_array(source: Callable[[], Optional[List[Any]]], map_fn: Callable[[Callable[[], Any], Callable[[], int]], T]) -> Callable[[], List[T]]

Map a reactive list with stable per-item scopes (keyed by identity).

Items are matched by reference identity. The mapping callback runs once per unique item; when an item leaves the source list, its reactive scope is disposed automatically.

Parameters:

Name Type Description Default
source Callable[[], Optional[List[Any]]]

Zero-arg getter that returns the current list (typically a signal accessor).

required
map_fn Callable[[Callable[[], Any], Callable[[], int]], T]

Called as map_fn(item_getter, index_getter) for each unique item. item_getter() returns the item; index_getter() returns its current position.

required

Returns:

Type Description
Callable[[], List[T]]

A zero-arg getter producing the mapped list. Reading it inside a

Callable[[], List[T]]

reactive scope subscribes to source changes.

Example
items, set_items = create_signal(["A", "B", "C"])
labels = map_array(items, lambda item, idx: f"{idx()}: {item()}")
# labels() == ["0: A", "1: B", "2: C"]

index_array

index_array(source: Callable[[], Optional[List[Any]]], map_fn: Callable[[Callable[[], Any], int], T]) -> Callable[[], List[T]]

Map a reactive list with stable per-index scopes.

Unlike map_array, scopes are keyed by index position. Each slot has a reactive item_getter signal that updates when the value at that index changes.

Parameters:

Name Type Description Default
source Callable[[], Optional[List[Any]]]

Zero-arg getter that returns the current list.

required
map_fn Callable[[Callable[[], Any], int], T]

Called as map_fn(item_getter, index). item_getter() returns the item; index is a plain int (not a getter).

required

Returns:

Type Description
Callable[[], List[T]]

A zero-arg getter producing the mapped list.

Example
items, set_items = create_signal(["A", "B", "C"])
labels = index_array(items, lambda item, idx: f"[{idx}] {item()}")
# labels() == ["[0] A", "[1] B", "[2] C"]

create_selector

create_selector(source: Callable[[], Any]) -> Callable[[Any], bool]

Create an O(1) selection signal.

When source() changes, only computations that previously called the returned is_selected(key) with the old or new key are notified, instead of every subscriber re-running.

Parameters:

Name Type Description Default
source Callable[[], Any]

Zero-arg getter returning the currently-selected key.

required

Returns:

Type Description
Callable[[Any], bool]

A function is_selected(key) -> bool that's reactive to

Callable[[Any], bool]

selection changes.

Example
selected, set_selected = create_signal(1)
is_selected = create_selector(selected)

# Inside a For loop per item:
create_effect(lambda: print("active:", is_selected(item_id)))

Ownership classes

Owner

Base reactive ownership scope. Tracks child owners and cleanup callbacks.

Attribute Description
_parent Parent Owner or None for roots.
_children List of child Owner instances.
_cleanups Callbacks run on disposal (LIFO order).
_context_map Optional dict mapping context IDs to values (used by Provider).
Method Description
dispose() Dispose children depth-first, run own cleanups, detach from parent.
_lookup_context(ctx_id, default) Walk up the owner chain to find a context value.
_set_context(ctx_id, value) Store a context value on this owner.
Computation(Owner)

Reactive computation (an effect or a memo) that tracks its sources (signals and other memos) and re-runs when they change. Also an ownership scope: child computations created during execution are disposed before each re-run.

Each computation carries a state for the push-mark / pull-recompute scheduler: CLEAN (current), CHECK (a transitive source may have changed), or DIRTY (a direct source did change). A signal write pushes DIRTY/CHECK markers out to dependents and queues affected effects; effects then pull their sources up to date, which keeps updates glitch-free.

Method Description
_stale(state) Mark CHECK/DIRTY and propagate a CHECK to observers; queue the node when it's an effect transitioning from CLEAN.
_update_if_necessary() Resolve CHECK by pulling sources, then recompute if DIRTY. Marked CLEAN before recompute so a re-entrant self-write reschedules it.
_update() Dispose children and cleanups, rebuild the dependency set, and re-execute the function under _current_owner = _current_observer = self. For memos, store the new value and escalate observers to DIRTY only when it changed.
dispose() Unsubscribe from all sources, drop self as a source for any observers, dispose children, run cleanups.

Public API

  • create_signal(value, *, equals=...) -> (getter, setter). Optional equals: default uses value equality (==) with an identity (is) fast-path; equals=True is equivalent to the default; equals=False notifies on every set(); equals=fn with fn(old, new) -> bool skips notification when fn returns True (custom comparator). Use equals=lambda a, b: a is b for SolidJS-style identity-only semantics. The getter exposes .peek() for untracked reads. The setter also accepts an updater function: set_count(lambda n: n + 1) computes the new value from the previous one and returns the value it stored.
  • create_effect(fn) -> Computation. The returned Computation is added as a child of the current owner. Inside a component's setup phase the owner is the _ComponentContext (effect survives re-renders, disposed on unmount). Inside a render function the owner is the render Computation (effect disposed on re-render). Supports previous value: create_effect(lambda prev: ...). User effects run after render effects and after the DOM commit in each flush, matching Solid's createEffect timing.
  • create_render_effect(fn) -> Computation. Like create_effect, but queued in the render phase: it runs before the DOM commit and before user effects in each flush. The framework uses this internally for DOM bindings; use it when an effect must run during the render phase.
  • create_computed(fn) -> Computation. An eager, pure computation that runs before render effects in each flush, mirroring Solid's createComputed. Prefer create_memo for derived values; use create_computed to push a signal write that later work depends on.
  • create_memo(fn, *, equals=...) -> getter. Creates a lazy memo Computation under the current owner; recomputes only when read after a source changed, and is disposed when the owner is disposed. Optional equals (same semantics as create_signal) controls when the memo's own observers are notified after a recompute. The getter exposes .peek() for untracked reads.
  • create_reaction(on_invalidate) -> track. On-change reaction with manual tracking, mirroring Solid's createReaction. Call track(fn) to run fn with tracking; the first change to any tracked dependency fires on_invalidate (untracked) and stops tracking until track is called again.
  • on_error(handler). Registers an error handler on the current reactive owner scope, mirroring Solid's onError. Exceptions raised by child computations route to the nearest ancestor handler; multiple handlers in one scope chain in registration order.
  • create_deferred(source) -> getter. Returns a read-only getter that trails source: the new value is published on the next event-loop tick (immediately when no asyncio loop is running) so expensive consumers can lag behind rapid-fire updates, mirroring Solid's createDeferred.
  • create_unique_id() -> str. Returns a unique, stable ID string ("wyb-0", "wyb-1", ...) for wiring for/aria-* attributes, mirroring Solid's createUniqueId.
  • catch_error(fn, handler) -> result | None. Runs fn under a scope whose errors (including errors thrown later by effects created inside it) route to handler instead of propagating. Mirrors Solid's catchError.
  • on_mount(fn). Run after first render.
  • on_cleanup(fn). Appends fn to the current owner's cleanup list. Inside create_effect: runs before each re-execution and on disposal. Inside a component's setup phase: runs when the component unmounts.
  • batch() -> context manager or batch(fn) -> result. The callback form flushes synchronously.
create_signal and equals
from wybthon import create_signal

# Default: value equality (==) with an identity (is) fast-path.
# Re-setting an unchanged value is a no-op; a new container with
# value-equal contents also skips.  Mutating the same list/dict in
# place and re-setting the same reference is a no-op too -- copy
# the container first or pass ``equals=False`` to force notification.
x, set_x = create_signal({"a": 1})

# Equivalent to the default.
y, set_y = create_signal(0, equals=True)

# Always notify subscribers, even when the value is unchanged.
z, set_z = create_signal(0, equals=False)

# SolidJS-style identity-only semantics: notify whenever the new
# reference is not the same Python object as the old.
w, set_w = create_signal([], equals=lambda old, new: old is new)
ReactiveProps and get_props()

get_props() returns the ReactiveProps proxy for the current @component instance. The proxy exposes one consistent shape for every prop:

  • props.name (attribute) or props["name"] (item) returns a stable zero-arg accessor. Calling the accessor reads the current value (tracked when called inside an effect or hole). Embedding it in a VNode tree creates a reactive auto-hole.
  • props.value(name, default=None) reads the current value immediately (a one-shot, untracked-friendly snapshot).
  • The proxy supports in, len(), iteration, and == against dicts.
from wybthon import component, create_effect, dynamic, get_props, p

@component
def Greeting(name="world"):
    props = get_props()
    create_effect(lambda: print("name is now", props.name()))
    return p(dynamic(lambda: f"Hello, {props.name()}!"))

When a component declares a single positional parameter with no default, the decorator passes the proxy in directly (proxy mode); otherwise each parameter is bound to its own accessor and there's no need to call get_props().

ReactiveProps is read-only; the parent/reconciler updates underlying values. When a parent passes a getter (e.g., name=my_signal), the proxy unwraps it transparently, and children always read with props.name().

get_owner() and run_with_owner(owner, fn)

After an await, the reactive owner stack may no longer match the component that started the work. Capture the owner before awaiting and restore it when creating effects or other scoped work:

from wybthon import create_effect, get_owner, run_with_owner

async def load():
    owner = get_owner()
    data = await fetch_something()
    run_with_owner(owner, lambda: create_effect(lambda: use(data)))
children(fn)

children(getter) wraps a zero-argument callable that returns the children value (often lambda: get_props().children()) and returns a memo getter that flattens and resolves the list. Matches Solid's children() helper. Import under an alias (e.g., from wybthon import children as resolve_children) if your component also names a parameter children.

from wybthon import children, component, dynamic, get_props, h3, section

@component
def Card(title=""):
    props = get_props()
    resolved = children(lambda: props.children())
    return section(h3(dynamic(lambda: props.title())), *resolved(), class_="card")
Resources
  • create_resource(fetcher) -> Resource
  • create_resource(source, fetcher) -> Resource. Refetches when the source changes.
  • Both forms accept initial_value=...: the resource starts in the "ready" state with that data, and the first fetch behaves like a refresh (previous data stays readable, Suspense isn't triggered).

A Resource is callable, mirroring Solid's createResource accessor:

  • res() reads the resolved data (tracked). Reading it inside a Suspense boundary automatically registers the resource with that boundary while it's pending.
  • res.loading and res.error are tracked reads of the loading flag and last error.
  • res.latest returns the most recent resolved value even while a refetch is in flight (useful for keep-previous UIs).
  • res.state is one of "unresolved", "pending", "ready", "refreshing", or "errored".
  • res.mutate(value) writes the resolved value directly (optimistic updates); res.refetch() re-runs the fetcher; res.cancel() abandons an in-flight fetch.
from wybthon import Suspense, create_resource, create_signal, h

user_id, set_user_id = create_signal(1)
user = create_resource(user_id, fetch_user)   # refetches when user_id changes

view = Suspense(
    fallback="Loading...",
    children=lambda: h("p", {}, lambda: user()["name"]),
)
Reactive utilities
  • untrack(fn). Run without tracking signal reads.
  • on(deps, fn, defer=False). Effect with explicit deps.
  • create_root(fn). Creates an independent Owner root. fn receives a dispose callback that tears down the root and all its children. Effects created inside the root are owned by it and cleaned up on dispose().
  • merge_props(*sources). Merge prop sources into a reactive proxy. Each source may be a plain dict, a callable getter, or another proxy. Reads are lazy: callable sources are called on each access for signal tracking. Returns an object supporting [], .get(), in, len(), iteration, and == comparison with dicts.
  • split_props(props, *key_groups). Split a props source into reactive proxy groups by key name, plus a rest group. Returns (group1, ..., rest); each proxy lazily reads from the original source.
Reactive list primitives
  • map_array(source, map_fn). Keyed reactive list mapping. source is a getter returning a list; map_fn(item_getter, index_getter) runs once per unique item (matched by reference identity). Returns a getter producing the mapped list. Per-item reactive scopes are created and disposed automatically.
  • index_array(source, map_fn). Index-keyed reactive list mapping. Like map_array but keyed by index position. map_fn(item_getter, index: int): the item getter is a signal that updates in place. Returns a getter producing the mapped list.
  • create_selector(source). Efficient selection signal. Returns is_selected(key) -> bool. When the source changes, only the previous and new key's dependents re-run (O(1) instead of O(n)).
Global state

Two module globals drive tracking. _current_owner is the active ownership scope: effects and memos created while it's set become its children. _current_observer is the computation currently recording dependencies: a signal or memo read while it's set subscribes that observer. Computation._update() sets both to self while it executes, so reads link to the running computation and newly created scopes are owned by it. Effects flush synchronously through a shared queue; batch() defers that flush to the outermost batch boundary.

Type hints are provided for all public functions and classes.