Skip to content

Reactivity

wybthon.reactivity

reactivity

Wybthon's reactive system.

Signals, memos, effects, and the scheduler that ties them together. This package is pure Python and runs anywhere (CPython, Pyodide); nothing here touches the DOM. Import from wybthon in application code; the submodules are implementation detail.

Classes:

Name Description
Action

A wrapped mutation returned by action.

Accessor

A zero-argument callable that returns a reactive value.

Computation

A tracked function: the node type behind memos and effects.

Memo

Read-only derived value; what create_memo returns.

NotReadyError

Raised when reading an async computation that has no value yet.

Owner

A reactive ownership scope.

Signal

Mutable reactive container; the accessor half of create_signal.

Transition

A dependency group waiting to publish a consistent reactive version.

WriteInScopeError

Raised (in dev mode) when a signal is written inside a tracking scope.

ChildrenAccessor

The resolved children returned by children.

Setter

The write half of create_signal.

ParentProps

Props for a component that renders children.

Prop

A reactive component input, declared on a Props class.

Props

Base class for a component's declared inputs.

ServerError

An async memo failed on the server; raised again on the client while hydrating.

Functions:

Name Description
action

Wrap a mutation in a transaction.

affects

Declare, inside an action, which values the action is going to change.

create_optimistic

Create a value whose writes are optimistic.

until

Return an awaitable for the settled, truthy value of pred().

flush

Apply staged writes, run every dirty effect, and commit the DOM now.

get_observer

Return the computation currently tracking reads, or None.

get_owner

Return the active ownership scope, or None outside any scope.

is_accessor

Return True when value is a reactive expression: an Accessor or a zero-arg function.

run_with_owner

Run fn under owner (pass None for no ownership) and return its result.

untrack

Run fn without tracking any reactive reads.

map_array

Map a reactive list to mapped rows with per-row owner scopes.

repeat

Map an integer range to rows, each with its own owner scope.

children

Resolve a component's children once, so it can inspect or reuse them.

create_effect

Create a side effect that re-runs when its tracked sources change.

create_memo

Create a derived value that recomputes when its sources change.

create_owner

Create an ownership scope owned by the current one.

create_reaction

Separate tracking from re-execution: run effect once when tracked reads change.

create_render_effect

Create a split effect that runs in the render phase, before the DOM commit.

create_root

Run fn inside a root owned by the current scope unless explicitly detached.

create_signal

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

create_tracked_effect

Run a tracked callback after DOM commit, disposing its scope before each run.

create_unique_id

Return a unique id string (for for/id attribute pairs).

is_disposed

Return whether owner has been disposed.

is_pending

Return True while a change is in flight for the value fn reads.

is_server

Return True while a server render is in progress.

latest

Evaluate fn against the newest state, without ever suspending.

on_cleanup

Register fn to run when the active scope is disposed or re-runs.

on_settled

Run fn once the current render has settled and committed to the DOM.

refresh

Recompute a derived read quietly and return an awaitable for its settled value.

resolve

Return an awaitable for the next settled value of fn().

merge

Merge prop sources into one reactive mapping; later sources win.

omit

Return a reactive view of source without some keys.

prop

Declare a Prop[T] field's default.

Action

Action(fn: Callable[P, Any])

A wrapped mutation returned by action.

Calling it invokes the wrapped function inside a transaction. The pending accessor is True while any invocation is in flight; it reveals immediately, so it's the right thing to bind a disabled button or a spinner to.

Attributes:

Name Type Description
pending Accessor[bool]

Tracked accessor: True while an invocation is in flight.

pending instance-attribute

pending: Accessor[bool] = _core.run_with_owner(None, lambda: Memo(lambda: count() > 0))

Tracked accessor: True while an invocation is in flight.

Accessor

A zero-argument callable that returns a reactive value.

Calling an accessor inside a tracking scope (a memo, an effect's compute stage, or a reactive hole) subscribes that scope to the value. peek reads without subscribing.

Every reactive read in Wybthon goes through an Accessor: create_signal getters are Signals, create_memo returns a Memo, and component parameters are Props. Embedding an accessor in a VNode tree creates a reactive hole. Type it as Accessor[T] in signatures when you accept any of them.

Methods:

Name Description
peek

Return the current value without subscribing anything.

peek

peek() -> T

Return the current value without subscribing anything.

Computation

Computation(fn: Callable[..., Any], *, kind: int, value: Any = None, equals: Any = _DEFAULT_EQUALS, apply: Callable[..., Any] | None = None, defer: bool = False, error: Callable[[BaseException], Any] | None = None, lazy: bool = False, unobserved: Callable[[], Any] | None = None, pass_prev: bool | None = None, name: str | None = None, eager: bool = False, apply_scope: bool = True, data: bool = False, keep: bool = False)

Bases: Owner

A tracked function: the node type behind memos and effects.

A computation re-runs when a source it read changes. It's also an owner: children created during a run are disposed before the next run, so conditionally created effects never leak.

Kinds:

  • memo: carries a value that other computations can read.
  • render effect: runs in the render phase of a flush (holes and prop bindings) before the DOM commit.
  • effect: runs after the DOM commit and may have a split compute/apply shape.

Effects take part in transitions: when a re-run's compute stage read data a transition holds, the apply stage is postponed until the transition reveals. Eager effects (eager=True, used for projections and selectors whose apply stage produces more graph data) always apply immediately and hold what they write instead.

Methods:

Name Description
dispose

Permanently dispose a computation and its committed resources.

dispose

dispose() -> None

Permanently dispose a computation and its committed resources.

Memo

Memo(fn: Callable[..., T], *, equals: Any = _DEFAULT_EQUALS, lazy: bool = False, unobserved: Callable[[], Any] | None = None, name: str | None = None, loading_value: Any = _MISSING, ssr_source: str = 'server')

Bases: Computation, Accessor[T]

Read-only derived value; what create_memo returns.

A Memo is both a Computation (it tracks sources and recomputes lazily) and an Accessor (call it to read). Recomputation happens on read after a source changed, and observers are notified only when the recomputed value differs under the memo's equals policy.

Async bodies (async def, or an async generator) make the memo an async computation; see create_memo.

Methods:

Name Description
peek

Return the (up-to-date) value without subscribing; None while not ready.

peek

peek() -> T

Return the (up-to-date) value without subscribing; None while not ready.

NotReadyError

Bases: Exception

Raised when reading an async computation that has no value yet.

Propagates "not ready" through the graph: a memo whose body reads a pending async memo becomes pending itself, and a reactive hole that hits NotReadyError keeps its previous content while the nearest Loading boundary shows its fallback.

Application code rarely raises or catches this directly; use is_pending and latest to observe pending state without suspending.

Owner

Owner()

A reactive ownership scope.

Owners form a tree. Disposing an owner disposes its children first (depth-first), then runs its own cleanup callbacks LIFO. Every computation is an owner, as is each component instance, each For row, and each create_root.

Owners also carry context values (see create_context) and an optional error handler installed by Errored.

Methods:

Name Description
dispose

Tear down this owner and every descendant.

dispose

dispose() -> None

Tear down this owner and every descendant.

Children are disposed depth-first, then this owner's cleanups run in LIFO order, then it detaches from its parent. Later calls are no-ops.

Signal

Signal(value: T, *, equals: Any = _DEFAULT_EQUALS, unobserved: Callable[[], Any] | None = None, name: str | None = None)

Bases: Accessor[T]

Mutable reactive container; the accessor half of create_signal.

Reading a Signal (calling it) returns the committed value and subscribes the active tracking scope. set stages a write that becomes visible at the next flush.

While a transition holds the signal (its last write is waiting for async work to land), reads from outside the graph, such as event handlers, keep returning the value the UI shows; reads inside memos, effects, and holes see the new value.

Most code destructures create_signal into (getter, setter); Signal is public so it can appear in type hints and so self-contained reactive fields (as in Field) can carry their own setter.

Parameters:

Name Type Description Default
value T

The initial value.

required
equals Any

Equality policy; see create_signal.

_DEFAULT_EQUALS
unobserved Callable[[], Any] | None

Optional callback invoked when the signal loses its last subscriber (resource cleanup).

None
name str | None

Optional label used in dev-mode diagnostics.

None

Methods:

Name Description
peek

Return the committed value without subscribing.

set

Stage a write; the new value is visible after the next flush.

peek

peek() -> T

Return the committed value without subscribing.

set

set(value: T | Callable[[T], T]) -> T

Stage a write; the new value is visible after the next flush.

Supports functional updates: when value is callable it receives the latest value (including any write staged earlier in the same batch) and its result is stored, so two set(lambda n: n + 1) calls in one handler add two. To store a callable as the value, return it from an updater: set(lambda _: my_fn).

In dev mode, writing from inside a tracking scope raises WriteInScopeError.

Parameters:

Name Type Description Default
value T | Callable[[T], T]

The new value, or an updater (current) -> new.

required

Returns:

Type Description
T

The value that was staged.

Transition

Transition()

A dependency group waiting to publish a consistent reactive version.

Independent actions and async reads have independent groups. A shared write or ordinary consumer joins groups. Redirects preserve references captured by suspended actions when their groups join.

WriteInScopeError

Bases: RuntimeError

Raised (in dev mode) when a signal is written inside a tracking scope.

Writing a signal from a memo body, a single-function effect, or a reactive hole is almost always a bug: derive the value with create_memo instead, or move the write into the untracked apply stage of a split create_effect, an event handler, or an action.

ChildrenAccessor

ChildrenAccessor(memo: Memo[list[Any]])

Bases: Accessor[Any]

The resolved children returned by children.

Calling it returns the single resolved child, or a list when there are several (or none); to_array always returns a list.

Methods:

Name Description
to_array

Return the resolved children as a list (tracked).

to_array

to_array() -> list[Any]

Return the resolved children as a list (tracked).

Setter

Bases: Protocol

The write half of create_signal.

Call it with a new value, or with an updater (current) -> new for a functional update. Returns the value that was staged.

ParentProps

ParentProps(**values: Any)

Bases: Props

Props for a component that renders children.

Prop

Prop(field: _Field)

A reactive component input, declared on a Props class.

The parent may pass a T, an accessor of T, or a zero-argument function returning T. Reading the field on the component's props returns an Accessor: place it in the tree to create a reactive hole, call it inside a tracking scope, or use .peek() for an intentional one-time read.

Props

Props(**values: Any)

Bases: _PropsBase

Base class for a component's declared inputs.

Subclass it, annotate fields, and take one parameter of that type in the component. Every component also accepts key, which gives the instance a stable identity for keyed reconciliation.

At run time the reconciler builds one instance per mounted component. Reading a Prop[T] field returns an accessor that tracks the parent's current value, so a parent that later passes a new value (or an accessor) updates the child without re-running it.

ServerError

ServerError(message: str, type_name: str = 'Exception')

Bases: RuntimeError

An async memo failed on the server; raised again on the client while hydrating.

The original exception doesn't survive serialization, so the client sees its type name and message. An Errored boundary shows the same fallback on both sides; resetting the boundary runs the computation again in the browser.

action

action(fn: Callable[P, Awaitable[R]]) -> Action[P, Future[R]]
action(fn: Callable[P, R]) -> Action[P, R]
action(fn: Callable[..., Any]) -> Action

Wrap a mutation in a transaction.

Calling the returned Action invokes fn and holds a transition open until it settles. Inside:

  • Plain writes stage into the transaction. Signals and stores written by the action (before or after an await) commit to the graph but reveal together when the action settles, as does the landing of any memo it refreshes. Reads inside the action see the staged values.
  • Optimistic writes reveal now. Values written through create_optimistic or create_optimistic_store show immediately and revert when the action settles.
  • action.pending() is True from the call until it settles; affects marks other values as pending, and until waits for a condition on the authoritative view.

Independent actions settle separately. Nested actions and actions that write shared reactive state join and publish together.

Errors route to the nearest Errored boundary captured at call time and re-raise to the awaiter, so await my_action(...) behaves like a normal call.

Parameters:

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

The mutation; sync or async def.

required

Returns:

Type Description
Action

A callable Action with a tracked .pending accessor.

Example
todos, set_todos = create_store({"items": []})
shown, set_shown = create_optimistic_store(lambda: deep(todos)["items"], [])

@action
async def add_todo(title):
    set_shown(lambda s: s.append({"title": title, "saving": True}))
    saved = await api_create(title)
    set_todos(lambda s: s["items"].append(saved))

affects

affects(*targets: Any) -> None

Declare, inside an action, which values the action is going to change.

Until the action settles, is_pending reports True for expressions that read any of targets, even before the action has written anything. Use it when the pending state should show on the data being changed rather than on the action itself, for example to dim a record while a save is in flight.

Parameters:

Name Type Description Default
*targets Any

Signals, memos, or stores (a store proxy marks the selected subtree).

()

Raises:

Type Description
RuntimeError

If called outside an action's synchronous segment.

TypeError

For a target that isn't a reactive node or store.

Example
@action
async def rename(user_id, name):
    affects(users)
    await api_rename(user_id, name)
    refresh(users)

create_optimistic

create_optimistic(source: Accessor[T]) -> tuple[Accessor[T], Setter[T]]
create_optimistic(source: Callable[[], T]) -> tuple[Accessor[T], Setter[T]]
create_optimistic(source: T) -> tuple[Accessor[T], Setter[T]]
create_optimistic(source: Any) -> tuple[Accessor[Any], Setter[Any]]

Create a value whose writes are optimistic.

Reads return the override while one is active, else the source value. Writes reveal immediately, even inside an action whose other writes are held, and the override reverts when the transaction it was written in settles. While an override is active, is_pending reports True for expressions that read it, and until sees through it to the source.

Use it both for optimistic data (the like count you expect the server to confirm) and for process state that must show during an action (a "saving" flag).

Parameters:

Name Type Description Default
source Any

A zero-arg accessor to shadow, or a plain initial value.

required

Returns:

Type Description
tuple[Accessor[Any], Setter[Any]]

A (getter, setter) pair like create_signal.

Example
likes = create_memo(fetch_like_count)          # async source
shown, set_shown = create_optimistic(likes)     # shadows it

@action
async def like():
    set_shown(lambda n: (n or 0) + 1)   # instant UI
    await api_like()
    await refresh(likes)                # real data lands; the override reverts

until

until(pred: Callable[[], T], *, timeout: float | None = None) -> _Settle[T]

Return an awaitable for the settled, truthy value of pred().

pred is evaluated reactively against the authoritative view: optimistic overrides are invisible, so the condition observes real data only. Inside an action, reads see the action's own staged writes. A read that raises NotReadyError simply waits for the value.

Parameters:

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

Zero-arg predicate.

required
timeout float | None

Optional seconds after which the awaitable rejects with TimeoutError.

None
Example
@action
async def checkout(cart_id):
    order_id = await api_checkout(cart_id)
    refresh(orders)
    await until(lambda: any(o["id"] == order_id for o in orders()))
    navigate(f"/orders/{order_id}")

flush

flush(fn: Callable[[], T] | None = None) -> T | None

Apply staged writes, run every dirty effect, and commit the DOM now.

Writes become visible to reads only when the graph flushes. In the browser that happens automatically on a microtask and after every event handler; call flush() when you need the settled state now, for example right after a write in synchronous test code.

If the flush opens a transition (a write made an async computation that already had a value recompute), the affected parts of the UI stay on their previous state until that work lands; everything else reveals as usual.

Safe to call at any time; a no-op when nothing is pending, and a no-op inside an effect (the running flush finishes the work).

With fn, runs it first and then flushes, returning its result: flush(lambda: set_count(1)) applies the write synchronously.

Example
count, set_count = create_signal(0)
set_count(1)
count()   # 0: the write is staged
flush()
count()   # 1

get_observer

get_observer() -> Computation | None

Return the computation currently tracking reads, or None.

get_owner

get_owner() -> Owner | None

Return the active ownership scope, or None outside any scope.

Capture it before an await and restore it with run_with_owner when you need to create primitives after the boundary.

is_accessor

is_accessor(value: Any) -> bool

Return True when value is a reactive expression: an Accessor or a zero-arg function.

This is the single rule the framework uses to decide what becomes a reactive hole (as a child), a reactive binding (as a prop), or an auto-unwrapped component prop. Plain functions and lambdas qualify when they take no required positional arguments; bound methods likewise. Classes, Refs, components, and callbacks that take arguments do not.

run_with_owner

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

Run fn under owner (pass None for no ownership) and return its result.

untrack

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

Run fn without tracking any reactive reads.

Use it to read a signal inside an effect without depending on it, or to seed local state from a prop during component setup without subscribing (the dev-mode top-level-read warning is silenced inside untrack; Accessor.peek is the shorter form for a single read).

Parameters:

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

Zero-arg callable to invoke with tracking suppressed.

required

Returns:

Type Description
T

Whatever fn returns.

map_array

map_array(source: Callable[[], Sequence[T] | None], fn: Callable[..., U], *, keyed: bool | Callable[[T], Any] = True, fallback: Callable[[], U] | None = None) -> Memo[list[U]]

Map a reactive list to mapped rows with per-row owner scopes.

Rows are matched between updates according to keyed:

  • True (default): match by identity (scalars by value). A matched row keeps its scope and mapped result and its index accessor updates. fn(item, index) receives the raw item and an Accessor[int].
  • False: match by position. The row at each index is reused and its item accessor updates. fn(item, index) receives an Accessor[T] and an int.
  • a callable key(item) -> hashable: match by key. Both item and index are accessors: fn(item, index) receives Accessor[T] and Accessor[int].

Row bodies run untracked inside the row's owner; anything reactive inside a row must read an accessor within a hole, memo, or effect, and cleanups registered with on_cleanup run when the row is removed.

Parameters:

Name Type Description Default
source Callable[[], Sequence[T] | None]

Zero-arg accessor returning the list (or None).

required
fn Callable[..., U]

Row mapping function; see the shapes above.

required
keyed bool | Callable[[T], Any]

Matching strategy.

True
fallback Callable[[], U] | None

Optional zero-arg callable whose result is the single row when the list is empty.

None

Returns:

Type Description
Memo[list[U]]

A Memo yielding the list of mapped rows.

repeat

repeat(count: Callable[[], int] | int, fn: Callable[[int], U], *, start: Callable[[], int] | int = 0, fallback: Callable[[], U] | None = None) -> Memo[list[U]]

Map an integer range to rows, each with its own owner scope.

The counterpart of Solid 2.0's repeat: fn(i) runs once per slot for i in range(start, start + count). Growing the count maps only the new slots; shrinking it disposes the removed ones. Changing start maps every slot again, because each row received a plain int.

Parameters:

Name Type Description Default
count Callable[[], int] | int

The number of slots, or an accessor for it.

required
fn Callable[[int], U]

Maps one slot number to a row.

required
start Callable[[], int] | int

The first slot number, or an accessor for it.

0
fallback Callable[[], U] | None

Optional zero-arg callable whose result is the single row when the count is zero.

None

Returns:

Type Description
Memo[list[U]]

A Memo yielding the list of mapped rows.

children

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

Resolve a component's children once, so it can inspect or reuse them.

Wraps a getter that returns children (typically props.children) and returns a memoized accessor. Nested lists are flattened, reactive children (accessors and zero-argument functions) are called and their results resolved, and None and booleans are dropped. This matches Solid 2.0's children helper.

from wybthon import children as resolve_children


@component
def Tabs(props: ParentProps):
    tabs = resolve_children(props.children)
    return nav(lambda: [li(tab) for tab in tabs.to_array()])

create_effect

create_effect(compute: Callable[..., Any], apply: Callable[..., Any], *, defer: bool = False, error: Callable[[BaseException], Any] | None = None) -> Computation

Create a side effect that re-runs when its tracked sources change.

Effects run after the DOM has been committed, so they observe the updated document; the first run happens on the next flush (right after the component that created it has mounted), not at creation. Inside a component they're disposed on unmount.

Split effect: compute runs tracked and returns a value; apply runs untracked with (value, prev) and performs the side effect. Incidental reads inside apply never over-subscribe the effect, and signal writes belong there. apply may return a cleanup callable that runs before the next apply and on disposal.

Use create_tracked_effect when one callback should combine tracking and side effects. Split effects keep committed resources alive while replacement data is held.

If compute accepts a positional parameter it receives its previous return value (None on the first run). compute may be async def; awaits suspend the effect without blocking and reads after an await are still tracked.

Parameters:

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

The tracked stage.

required
apply Callable[..., Any]

Untracked side-effect stage receiving (value, prev) (or just (value,) if it declares one parameter). May return a cleanup callable.

required
defer bool

When True, skip the first apply (tracking still starts immediately).

False
error Callable[[BaseException], Any] | None

Optional handler receiving exceptions raised by compute (sync or async) instead of routing them to the nearest Errored boundary.

None

Returns:

Type Description
Computation

The underlying computation; call .dispose() to stop it.

Example
create_effect(count, lambda value, prev: print(prev, "->", value))

create_effect(
    lambda: name(),
    lambda value: (timer := start_timer(value), lambda: stop_timer(timer))[1],
    defer=True,
)
Note

Effects don't run during a server render; see is_server.

create_memo

create_memo(fn: Callable[..., AsyncIterator[T]], *, equals: Any = ..., lazy: bool = ..., unobserved: Callable[[], Any] | None = ..., name: str | None = ..., loading_value: T = ..., ssr_source: SsrSource = ...) -> Memo[T]
create_memo(fn: Callable[..., Awaitable[T]], *, equals: Any = ..., lazy: bool = ..., unobserved: Callable[[], Any] | None = ..., name: str | None = ..., loading_value: T = ..., ssr_source: SsrSource = ...) -> Memo[T]
create_memo(fn: Callable[..., T], *, equals: Any = ..., lazy: bool = ..., unobserved: Callable[[], Any] | None = ..., name: str | None = ..., loading_value: T = ..., ssr_source: SsrSource = ...) -> Memo[T]
create_memo(fn: Callable[..., Any], *, equals: Any = _DEFAULT_EQUALS, lazy: bool = False, unobserved: Callable[[], Any] | None = None, name: str | None = None, loading_value: Any = _MISSING, ssr_source: SsrSource = 'server') -> Memo[Any]

Create a derived value that recomputes when its sources change.

Memos evaluate once at creation unless lazy=True. Updates are pull-based: the body runs when the memo is read after a tracked source changed, and observers are notified only when the new value differs under equals. If fn accepts a positional parameter it receives the previous value (None on the first run).

Async memos. When fn is an async def (or returns an awaitable), the memo becomes an async computation: reading it before the first value raises NotReadyError, which the nearest Loading boundary turns into fallback UI. Once it has a value, a recompute caused by an input change opens a transition: readers keep the previous value and the parts of the UI that depend on the changed input stay as they are until the new value lands, so the screen never shows a new input next to old data. Use is_pending to show a refresh hint and latest to read the new state early. Reads after an await are tracked exactly like reads before it.

Async generators. An async def body containing yield streams: each yielded value becomes the memo's new value. Use it to adapt sockets, subscriptions, or any async iterable.

Parameters:

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

Zero- or one-arg callable producing the value (sync, async, or an async generator).

required
equals Any

Equality policy; see create_signal.

_DEFAULT_EQUALS
lazy bool

When True, the memo suspends once it loses its last subscriber (and recomputes fresh if read again later). Non-lazy memos live for their owner's lifetime.

False
unobserved Callable[[], Any] | None

Optional callback fired when the memo loses its last subscriber; pair it with lazy=True for resource cleanup.

None
name str | None

Optional label used in dev-mode diagnostics.

None
loading_value Any

For async memos, a value to serve while the first run is in flight instead of raising NotReadyError. The memo is then never "not ready": it doesn't register with Loading, doesn't report pending for its first run, and never holds a transition on mount. Use it for nice-to-have data (a recommendation panel, a badge count).

_MISSING
ssr_source SsrSource

Where an async memo resolves when the page is server-rendered. "server" (the default) resolves it on the server and hydrates the browser with that value; "hybrid" does the same, then always re-runs it quietly in the browser; "client" never runs it on the server, so it loads after hydration. See Server rendering.

'server'

Returns:

Type Description
Memo[Any]

A Memo accessor.

Example
doubled = create_memo(lambda: count() * 2)

async def load_user():
    uid = user_id()          # tracked: refetches when it changes
    return await fetch_json(f"/api/users/{uid}")

user = create_memo(load_user)
suggestions = create_memo(load_suggestions, loading_value=[])

create_owner

create_owner() -> Owner

Create an ownership scope owned by the current one.

The scope is disposed with its parent. Run code under it with run_with_owner, and dispose it early with owner.dispose(). Pass it to run_with_owner(None, ...) first for a scope with an independent lifetime.

create_reaction

create_reaction(effect: Callable[[], Any], *, error: Callable[[BaseException], Any] | None = None) -> Callable[[Callable[[], Any]], None]

Separate tracking from re-execution: run effect once when tracked reads change.

Returns track(fn). Calling track runs fn and subscribes to what it reads; the first time any of those sources changes, effect runs (untracked, after the DOM commit) and the subscription ends. Call track again to re-arm it. Matches Solid 2.0's createReaction.

track = create_reaction(lambda: print("count changed"))
track(lambda: count())

create_render_effect

create_render_effect(compute: Callable[..., Any], apply: Callable[..., Any], *, defer: bool = False, error: Callable[[BaseException], Any] | None = None) -> Computation

Create a split effect that runs in the render phase, before the DOM commit.

Wybthon's reactive holes and prop bindings are render effects, so a render effect observes the DOM in the same state the framework's own bindings do (updates emitted, not yet committed). Prefer create_effect unless you're building a rendering primitive.

Like Solid 2.0's createRenderEffect, it always takes a tracked compute stage and an untracked apply stage, with the same arguments as create_effect.

create_root

create_root(fn: Callable[[Callable[[], None]], T], *, detached: bool = False) -> T

Run fn inside a root owned by the current scope unless explicitly detached.

Use it for explicitly disposable reactive work. Pass detached=True for global stores or subscriptions that outlive the surrounding component.

Parameters:

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

Receives a dispose callable that tears the root down.

required
detached bool

Create an independent lifetime instead of joining the current owner.

False

Returns:

Type Description
T

Whatever fn returns.

create_signal

create_signal(value: T | Callable[[], T], *, equals: Any = _DEFAULT_EQUALS, name: str | None = None, owned_write: bool = False) -> tuple[Accessor[T], Setter[T]]

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

Writes are staged: the setter records the new value and every read keeps returning the committed value until the next flush (a browser microtask, the end of an event handler, or an explicit flush). There is no batch(); everything batches.

The setter supports functional updates: pass lambda n: n + 1 and it receives the latest staged value, so repeated updates in one handler compose. To store a callable as the value, return it from an updater: set_fn(lambda _: my_callable).

Function form. When value is a zero-argument callable, the result is a writable derived signal: the getter tracks whatever the function reads and recomputes when those sources change, while the setter overrides the value until the next source change.

Parameters:

Name Type Description Default
value T | Callable[[], T]

The initial value, or a zero-arg function for the derived form.

required
equals Any

Equality policy deciding when subscribers are notified:

  • default: identity fast path, then ==. Re-setting an equal value is a no-op.
  • False: always notify, even for equal values.
  • a callable (old, new) -> bool: skip notification when it returns True. Pass lambda a, b: a is b for identity-only semantics.
_DEFAULT_EQUALS
name str | None

Optional label used in dev-mode diagnostics.

None
owned_write bool

Allow writes from inside an owned scope (a memo, an effect's compute stage, or a reactive hole) without raising WriteInScopeError. Reserve it for state that's genuinely local to that scope, such as a measurement a memo caches for itself.

False

Returns:

Type Description
Accessor[T]

A (getter, setter) tuple. The getter is an

Setter[T]

Accessor (call it to read, .peek() to

tuple[Accessor[T], Setter[T]]

read untracked); the setter is a Setter.

Example
count, set_count = create_signal(0)
set_count(5)
count()                     # 0: staged, not yet visible
flush()
count()                     # 5
set_count(lambda n: n + 1)  # functional update
count.peek()                # 5 (untracked read of the committed value)

doubled, _ = create_signal(lambda: count() * 2)   # derived form

create_tracked_effect

create_tracked_effect(fn: Callable[..., Any], *, error: Callable[[BaseException], Any] | None = None) -> Computation

Run a tracked callback after DOM commit, disposing its scope before each run.

Prefer split effects for external resources: a tracked callback can run while async work is pending. Reads subscribe; put writes in a split effect's apply stage. Doesn't run during a server render.

create_unique_id

create_unique_id() -> str

Return a unique id string (for for/id attribute pairs).

Ids created while a server render or a hydration mounts are derived from the component's position in the tree (wyb-h followed by a short hash), so the server and the hydrating browser agree on them.

is_disposed

is_disposed(owner: Owner) -> bool

Return whether owner has been disposed.

is_pending

is_pending(fn: Callable[[], Any]) -> bool

Return True while a change is in flight for the value fn reads.

Evaluates fn in probe mode and reports whether any value it read is waiting on a transition:

  • a signal or memo whose new value is held (the batch that changed it is waiting for async work to land, or the write was made inside an action that hasn't settled);
  • an async computation whose recompute is in flight (a quiet refresh is silent);
  • a value an in-flight action declared with affects;
  • an optimistic value with an active override;
  • a read that raises NotReadyError.

The reads are tracked like any other, so a hole using is_pending updates as the state changes, but they don't make the hole wait for the transition: an indicator has to show during the hold.

span(lambda: "Refreshing..." if is_pending(user) else "")
button("Save", disabled=lambda: is_pending(lambda: store.items))

is_server

is_server() -> bool

Return True while a server render is in progress.

Use it to choose a data source, or to skip browser-only work, in code that also runs in the browser:

async def load_user():
    if is_server():
        return database.users.get(user_id())
    return await fetch_json(f"/api/users/{user_id()}")

latest

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

Evaluate fn against the newest state, without ever suspending.

Two things differ from a plain read:

  • Not-ready async reads return their most recent value (or None if they never resolved) instead of raising NotReadyError.
  • Values a transition holds return the new value being computed rather than the one the UI still shows. Use it when a piece of UI should update ahead of the rest (a header that shows the newly selected id while the detail pane keeps the old record).

on_cleanup

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

Register fn to run when the active scope is disposed or re-runs.

  • Inside an effect body: runs before each re-run and on disposal.
  • Inside a component body: runs when the component unmounts.
  • Inside a reactive hole or For row: runs when that region is re-evaluated or torn down.

Raises:

Type Description
RuntimeError

If called outside any reactive scope.

on_settled

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

Run fn once the current render has settled and committed to the DOM.

The replacement for on_mount: inside a component body, fn runs after the flush that mounted the component has finished, so refs are assigned and the DOM is live. fn may return a cleanup callable, which runs when the owning scope is disposed (on unmount).

Doesn't run during a server render.

Parameters:

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

Zero-arg callback. May return a cleanup callable.

required

Raises:

Type Description
RuntimeError

If called outside any reactive scope.

Example
class ChartProps(Props):
    data: Prop[list[float]]


@component
def Chart(props: ChartProps):
    canvas = Ref()

    def start():
        handle = draw(canvas.current, props.data.peek())
        return lambda: handle.destroy()

    on_settled(start)
    return canvas_(ref=canvas)

refresh

refresh(target: Any) -> Awaitable[Any]

Recompute a derived read quietly and return an awaitable for its settled value.

"Quiet" means no pending state is reported while the run is in flight: is_pending stays False and readers keep the previous value. Use it after a server write to re-ask for data derived from the source of truth.

Called inside an action, the refreshed value lands into the action's transaction and reveals together with the action's other writes when it settles.

Parameters:

Name Type Description Default
target Any

A Memo (including async memos and function-form signals) or a derived store / projection.

required

Returns:

Type Description
Awaitable[Any]

An awaitable resolving with the target's next settled value.

Awaitable[Any]

Safe to ignore for fire-and-forget use.

resolve

resolve(fn: Callable[[], T]) -> Awaitable[T]

Return an awaitable for the next settled value of fn().

Tracks every dependency of fn, including cached memos, and waits for pending or quiet async recomputations. Streams become ready at their first fresh yield. Rejects when the expression raises; cancellation removes the temporary subscriptions.

user = create_memo(fetch_user)
data = await resolve(user)

merge

merge(*sources: Any) -> Mapping[str, Accessor[Any]]

Merge prop sources into one reactive mapping; later sources win.

Each source may be a component's props, a plain dict (values may be accessors or static), a zero-arg function returning a dict, or another merged or omitted view. Reads resolve right to left at access time, so tracking flows through to whichever source supplied the key. A key present with the value None overrides earlier sources, as an explicit undefined does in Solid 2.0.

The result is a mapping of accessors: spread it onto an element (button(**merge(defaults, extra))).

Example
attrs = merge({"type": "button"}, {"disabled": props.busy})
button("Save", **attrs)

omit

omit(source: Any, *keys: str | Callable[[str], bool]) -> Mapping[str, Accessor[Any]]

Return a reactive view of source without some keys.

Pass key names, or a single predicate that returns True for each key to drop. The replacement for Solid 1.x's splitProps: handle some props locally and forward the rest.

rest = omit(props, "label", "children")
rest = omit(props, lambda key: key.startswith("on_"))

prop

prop(*, default: T) -> Prop[T]
prop(*, default_factory: Callable[[], T]) -> Prop[T]
prop(*, default: Any = _MISSING, default_factory: Callable[[], Any] | None = None) -> Any

Declare a Prop[T] field's default.

Type checkers only recognize a field specifier's default when it's passed by keyword, so write prop(default=0). Use default_factory for mutable defaults.

class CounterProps(Props):
    step: Prop[int] = prop(default=1)
    tags: Prop[list[str]] = prop(default_factory=list)

What's in this module

wybthon.reactivity is the pure-Python reactive graph: signals, memos, effects, ownership, the scheduler, async computations, actions, and the props utilities that components are built on. Nothing here touches the DOM, so everything works in CPython as well as Pyodide. Import from wybthon in application code; the _core, _primitives, _actions, _list, and _props submodules are implementation detail.

Three rules shape every API here. Writes are staged: a setter records a value, and reads keep returning the committed value until the next flush (a microtask, the end of an event handler, or an explicit flush). Effects are deferred: create_effect runs for the first time on the next flush, after the DOM has been committed, not at creation. Async work runs in transitions: when a change makes an async memo recompute, the UI that depends on the change holds on the old state until the new value lands, and an action's writes reveal together when it settles.

Typed core

Name Description
Accessor Zero-arg callable returning a reactive value; .peek() reads untracked.
Setter Protocol for the write half of a signal; accepts a value or an updater.
Signal Mutable container behind create_signal; the getter is a Signal.
Memo Read-only derived value returned by create_memo.
Props Base class for a component's typed inputs (PEP 681 dataclass_transform, so pyright and mypy check calls without a plugin).
ParentProps Props with a children field.
Prop Field annotation Prop[T] marking a reactive input; reading it returns an Accessor[T] that unwraps whatever the parent passed.
ChildrenAccessor What children() returns: call it for one child or a list, or .to_array() for a list.
Owner Ownership scope; disposing it disposes children and runs cleanups.
Computation Tracked function node behind memos and effects; .dispose() stops it. Import from wybthon.reactivity.
Action Wrapped mutation returned by action; .pending is a tracked accessor.
Transition The open transaction holding in-flight changes (exposed for tooling; no public methods). Import from wybthon.reactivity.
NotReadyError Raised when reading an async computation that has no value yet.
WriteInScopeError Raised in dev mode when a signal or store is written inside a tracking scope.

Primitives

Name Description
create_signal (getter, setter) pair; equals= policy; function form makes a writable derived signal.
create_memo Glitch-free derived value, eager initially unless lazy=True; async def bodies become async computations; loading_value= serves a value before the first run lands.
create_effect Side effect after DOM commit; requires tracked compute and untracked apply.
create_tracked_effect Explicit combined tracking and side-effect callback.
create_render_effect create_render_effect(compute, apply): effect in the render phase, before the DOM commit; first run is immediate. apply is required.
create_reaction create_reaction(effect, *, error=None) returns track(fn): the first change to what fn read runs effect once.
on_settled Run once after the flush that mounted the component; may return a cleanup.
on_cleanup Register a cleanup on the active scope.
create_root Owned root; fn(dispose), with explicit detached=True for independence.
flush Apply staged writes, run dirty effects, commit the DOM now.
untrack Run a function without tracking its reads.
get_owner, run_with_owner Capture the owner before an await and restore it after.
get_observer The computation currently tracking reads, or None.
is_pending Tracked probe: True while what fn reads is held by a transition, recomputing, declared with affects, or optimistically overridden.
latest Evaluate against the newest state: held values return the value being computed; not-ready reads return the stale value or None.
refresh Quietly recompute a memo or derived store; returns an awaitable. Inside an action, lands with the action.
resolve Awaitable for the next settled value of an expression.
action Wrap a mutation in a transaction: its writes reveal together when it settles.
create_optimistic Value override that reveals now and reverts when the action settles.
affects Inside an action, mark values as pending before they're written.
until Awaitable for a predicate to become truthy on the authoritative (non-optimistic) view; timeout= raises the built-in TimeoutError.
prop prop(default=...) or prop(default_factory=...): declare a Prop[T] field's default.
merge, omit Reactive mapping views over props instances or dicts (later sources win; drop keys, or keys matching a predicate), ready to spread onto elements.
children ChildrenAccessor that resolves nested accessors, flattens lists, and drops None and booleans.
map_array Reactive value mapping with per-row scopes and an optional fallback; DOM lists use mounted regions.
repeat repeat(count, fn, *, start=0, fallback=None): reactive mapping over a range of indices.
create_unique_id Process-unique id string for id/for pairs.
is_accessor True for an Accessor or a zero-arg function.

Idioms

from wybthon import create_effect, create_memo, create_signal, flush

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

create_effect(doubled, lambda value, prev: print(prev, "->", value))

set_count(lambda n: n + 1)
count()  # 0: the write is staged
flush()  # commits the write, runs the effect: prints "None -> 2"
count()  # 1

Async data is an ordinary memo with an async def body. Reads before the first value raise NotReadyError, which the nearest Loading boundary turns into fallback UI; later recomputes open a transition that holds the dependent UI on the old state until the new value lands.

from wybthon import action, create_memo, create_optimistic, is_pending, refresh, span


async def load_likes():
    return await fetch_like_count()


likes = create_memo(load_likes)
shown, set_shown = create_optimistic(likes)


@action
async def like():
    set_shown(lambda n: (n or 0) + 1)
    await api_like()
    await refresh(likes)


hint = span(lambda: "Refreshing..." if is_pending(likes) else "")
count = span(t"{shown} likes")

Internally, two module globals (the active owner and the active observer) drive tracking and ownership; get_owner() and get_observer() expose them read-only.

See also