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 |
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 |
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 |
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 |
Setter |
The write half of |
ParentProps |
Props for a component that renders |
Prop |
A reactive component input, declared on a |
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 |
flush |
Apply staged writes, run every dirty effect, and commit the DOM now. |
get_observer |
Return the computation currently tracking reads, or |
get_owner |
Return the active ownership scope, or |
is_accessor |
Return True when |
run_with_owner |
Run |
untrack |
Run |
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 |
create_render_effect |
Create a split effect that runs in the render phase, before the DOM commit. |
create_root |
Run |
create_signal |
Create a reactive signal and return its |
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 |
is_disposed |
Return whether |
is_pending |
Return True while a change is in flight for the value |
is_server |
Return True while a server render is in progress. |
latest |
Evaluate |
on_cleanup |
Register |
on_settled |
Run |
refresh |
Recompute a derived read quietly and return an awaitable for its settled value. |
resolve |
Return an awaitable for the next settled value of |
merge |
Merge prop sources into one reactive mapping; later sources win. |
omit |
Return a reactive view of |
prop |
Declare a |
Action
¶
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: |
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. |
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. |
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; |
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
¶
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
¶
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 |
_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. |
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 |
required |
Returns:
| Type | Description |
|---|---|
T
|
The value that was staged. |
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
¶
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.
Prop
¶
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
¶
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
¶
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 itrefreshes. Reads inside the action see the staged values. - Optimistic writes reveal now. Values written through
create_optimisticorcreate_optimistic_storeshow immediately and revert when the action settles. action.pending()isTruefrom the call until it settles;affectsmarks other values as pending, anduntilwaits 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 |
required |
Returns:
| Type | Description |
|---|---|
Action
|
A callable |
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. |
create_optimistic
¶
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 |
until
¶
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
|
None
|
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.
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
¶
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 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 |
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 anAccessor[int].False: match by position. The row at each index is reused and its item accessor updates.fn(item, index)receives anAccessor[T]and anint.- a callable
key(item) -> hashable: match by key. Both item and index are accessors:fn(item, index)receivesAccessor[T]andAccessor[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 |
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 |
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 |
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.
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
|
required |
defer
|
bool
|
When |
False
|
error
|
Callable[[BaseException], Any] | None
|
Optional handler receiving exceptions raised by
|
None
|
Returns:
| Type | Description |
|---|---|
Computation
|
The underlying computation; call |
Example
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[..., 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 |
_DEFAULT_EQUALS
|
lazy
|
bool
|
When |
False
|
unobserved
|
Callable[[], Any] | None
|
Optional callback fired when the memo loses its last
subscriber; pair it with |
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 |
_MISSING
|
ssr_source
|
SsrSource
|
Where an async memo resolves when the page is
server-rendered. |
'server'
|
Returns:
| Type | Description |
|---|---|
Memo[Any]
|
A |
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.
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
¶
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 |
required |
detached
|
bool
|
Create an independent lifetime instead of joining the current owner. |
False
|
Returns:
| Type | Description |
|---|---|
T
|
Whatever |
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_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 |
False
|
Returns:
| Type | Description |
|---|---|
Accessor[T]
|
A |
Setter[T]
|
|
tuple[Accessor[T], Setter[T]]
|
read untracked); the setter is a |
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_pending
¶
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
actionthat hasn't settled); - an async computation whose recompute is in flight (a quiet
refreshis 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.
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
Noneif they never resolved) instead of raisingNotReadyError. - 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
¶
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
Forrow: runs when that region is re-evaluated or torn down.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If called outside any reactive scope. |
on_settled
¶
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. |
refresh
¶
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 |
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
¶
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.
merge
¶
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))).
omit
¶
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.
prop
¶
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.
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¶
- Concepts: Reactivity
- Concepts: Primitives
- Concepts: Lifecycle and ownership
- Concepts: Async and loading
- Guides: Testing (driving flushes in CPython)
- Guides: Typing