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. Every reactive computation (effect, memo) is 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 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)  # logs "doubled: 4" on the next microtask flush

Classes:

Name Description
Owner

Base reactive ownership scope.

Computation

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

Signal

Mutable reactive container that notifies subscribed computations on change.

ReactiveProps

Reactive proxy over a component's props dict.

Resource

Async resource with reactive data, error, and loading signals.

Functions:

Name Description
read_prop

Return the current value for key from props.

iter_prop_keys

Return the list of prop keys present on props.

batch

Batch signal updates so subscribers flush once at the end.

untrack

Run fn without tracking any signal reads.

create_resource

Create an async Resource with cancellation support.

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_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 List[Owner]

List of child owners.

_cleanups List[Callable[[], Any]]

Callbacks invoked LIFO during disposal.

_disposed bool

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

_context_map Optional[Dict[int, Any]]

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

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

Bases: Owner

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

Also serves as 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 run().

_deps Set[Signal[Any]]

Set of signals this computation currently subscribes to. Cleared and rebuilt on every run().

Methods:

Name Description
run

Re-execute the tracked function, refreshing its dependency set.

schedule

Enqueue this computation for the next flush.

dispose

Dispose the computation and unsubscribe from all dependencies.

run

run() -> None

Re-execute the tracked function, refreshing its dependency set.

Disposes child owners and runs cleanups before each re-run so that conditional effects don't leak. The previous dependency edges are torn down and rebuilt while the body executes under this owner.

schedule

schedule() -> None

Enqueue this computation for the next flush.

Called by Signal.set when a tracked dependency changes. When no batch is active a microtask flush is scheduled; otherwise the computation is held until the outermost batch completes.

dispose

dispose() -> None

Dispose the computation and unsubscribe from all dependencies.

Removes this computation from any pending flush queue, clears dependency edges, and tears down child owners and cleanups.

Signal

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

Bases: Generic[T]

Mutable reactive container that notifies subscribed computations 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.

set

Write a new value and notify subscribers 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.

set

set(value: T) -> None

Write a new value and notify subscribers 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).

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 is invoked and the result returned, mirroring _make_getter. If default is provided, it is 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 is 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)

Bases: Generic[R]

Async resource with reactive data, error, and loading signals.

Wraps an awaitable fetcher and exposes signal-backed state so consumers can render loading and error UIs declaratively (typically with Suspense).

Use reload() to (re)fetch and cancel() to abort the in-flight request.

When constructed with a source getter, the resource automatically refetches when the source's tracked value changes (skipping the very first read so it doesn't double-fetch on creation).

Attributes:

Name Type Description
data Signal[Optional[R]]

Reactive accessor for the most recent successful payload, or None before any fetch completes.

error Signal[Optional[Any]]

Reactive accessor for the most recent exception, or None.

loading Signal[bool]

Reactive accessor; True while a fetch is in flight.

Example
async def load_user(signal=None):
    resp = await fetch("/api/users/1")
    return await resp.json()

user = create_resource(load_user)
h("p", {}, dynamic(lambda: "Loading..." if user.loading() else user.data().get("name")))

Methods:

Name Description
reload

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

cancel

Abort the current in-flight fetch, if any.

reload

reload() -> None

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

Bumps the internal version, sets loading to True, clears error, and dispatches the fetcher on the asyncio loop. 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 data or error.

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.

read_prop

read_prop(props: Any, key: str, default: Any = None) -> Any

Return the current value for key from props.

Works with both ReactiveProps (the common case inside the reconciler) and plain dicts (test paths and a few legacy call sites). When called inside a tracking scope, the read is tracked against the underlying signal, mirroring the auto-unwrap behavior of ReactiveProps.value.

Parameters:

Name Type Description Default
props Any

A ReactiveProps proxy or any dict-like object.

required
key str

Prop name to read.

required
default Any

Returned when key is absent. Defaults to None.

None

Returns:

Type Description
Any

The current value, with auto-unwrap applied for ReactiveProps.

iter_prop_keys

iter_prop_keys(props: Any) -> List[str]

Return the list of prop keys present on props.

Same uniform shim as read_prop for components that iterate over an unknown prop bag (e.g. Link forwarding extra attributes onto the rendered <a>).

Parameters:

Name Type Description Default
props Any

A ReactiveProps proxy or any object with .keys().

required

Returns:

Type Description
List[str]

A list of present keys, or an empty list when props is not

List[str]

dict-like.

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 subscribers 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 before batch returns, 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) -> Resource[R]

Create an async Resource with cancellation support.

Can be called two ways:

  • create_resource(fetcher): simple fetcher, no source signal.
  • create_resource(source, fetcher): refetches automatically when the source getter's tracked value changes.

The fetcher should be an async function returning the data value. If it accepts a signal keyword argument, an AbortSignal is passed for cancellation support when running in a 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

Returns:

Type Description
Resource[R]

A Resource[R] whose data, error, and loading signals can

Resource[R]

be read inside reactive scopes.

Example
user_id, set_user_id = create_signal(1)

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

user = create_resource(user_id, load_user)

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 is no cursor system or "rules of hooks".

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.

Example
count, set_count = create_signal(0)
print(count())          # 0
set_count(5)
print(count())          # 5

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_memo

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

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

Re-computes only when signals read inside fn change. 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

Returns:

Type Description
Callable[[], T]

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

Callable[[], T]

a dependency on the memoised value.

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

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 should not 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 is 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 is 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 that tracks Signal reads and re-runs when they change. Also an ownership scope: child computations created during execution are disposed before each re-run.

Method Description
run() Dispose children and cleanups, clear deps, re-execute the function under _current_owner = self.
schedule() Enqueue a re-run on the next flush.
dispose() Cancel subscriptions, clear deps, remove from pending queue, 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.
  • 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: ...).
  • create_memo(fn) -> getter. Creates a Computation under the current owner; disposed when the owner is disposed.
  • 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 is 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.
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

A single _current_owner global tracks the active ownership scope. When a Computation.run() executes, it sets _current_owner = self so that any effects or memos created during execution become its children.

Type hints are provided for all public functions and classes.