Skip to content

Flow control

wybthon.flow

flow

Control flow: Show, For, Repeat, Switch/Match, dynamic, and client_only.

These primitives create isolated reactive scopes so only the relevant subtree updates when a condition or list changes. Each is a function returning a component VNode; conditions and sources are accessors (or plain values), and children/fallback slots are VNodes or callables evaluated inside the primitive's own scope.

Callback shapes follow SolidJS 2.0:

  • Show(when, children): a callable children receives an Accessor for the truthy value (or the value itself with keyed=True).
  • For(each, children, keyed=True): children(item, index) where the shapes depend on keyed (see For).
  • Repeat(count, children): children(index: int).
  • Switch(Match(when, children), ..., fallback=...).
  • dynamic(source): a component whose implementation is chosen reactively.
  • client_only(children, fallback=...): content that only renders in the browser, after hydration.
Example
Show(is_logged_in, lambda: p("Welcome!"), fallback=lambda: p("Please log in"))

For(todos, lambda todo, i: li(todo["title"]))

Repeat(rating, lambda i: span("*"))

Classes:

Name Description
Match

A branch of a Switch.

Functions:

Name Description
Show

Render children while when is truthy, else fallback.

For

Render a list with a stable subtree per row.

Repeat

Render children(i) for i in range(start, start + count) with no diffing.

Switch

Render the first Match whose condition is truthy.

dynamic

Turn an accessor for a component (or tag) into a component you can call.

client_only

Render children only in the browser, after hydration.

is_hydrating

Whether the browser is adopting server-rendered DOM right now.

NoHydration

Render children on the server as static HTML the browser won't hydrate.

Hydration

Mark a subtree for hydration; a passthrough in Wybthon.

Match

Match(when: Any, children: Any = None, *, keyed: bool = False)

A branch of a Switch.

Parameters:

Name Type Description Default
when Any

Condition accessor or plain value.

required
children Any

VNode, zero-arg callable, or (value) -> VNode receiving an Accessor (or the raw value with keyed=True).

None
keyed bool

Re-create the branch on every value change.

False

Show

Show(when: Any, children: Any = None, fallback: Any = None, *, keyed: bool = False) -> VNode

Render children while when is truthy, else fallback.

Only the truthiness of when is tracked by default: the branch re-renders when the condition flips, not on every value change. A callable children may accept one argument, an Accessor for the (truthy) value, so inner holes can read it reactively.

With keyed=True, children re-renders whenever the value itself changes and receives the raw value.

Parameters:

Name Type Description Default
when Any

Condition accessor or plain value.

required
children Any

VNode, zero-arg callable, or (value) -> VNode.

None
fallback Any

Rendered when when is falsy.

None
keyed bool

Re-create the branch on every value change.

False
Example
Show(user, lambda u: p("Hello, ", lambda: u().name), fallback=p("Sign in"))

For

For(each: Any, children: Callable[..., Any], fallback: Any = None, *, keyed: bool | Callable[[Any], Any] = True) -> VNode

Render a list with a stable subtree per row.

The mapping callback runs once per row; when the list changes, existing rows keep their DOM and are only moved, never re-diffed. A row's reactive scope is disposed when it leaves the list.

keyed selects how rows are matched, and with it the callback shape:

  • True (default): match by identity (scalars by value). children(item, index) receives the raw item and an Accessor[int] index.
  • False: match by position. children(item, index) receives an Accessor for the item at that position and an int index.
  • a callable key(item): match by key, updating the row in place when a new object has the same key. children(item, index) receives accessors for both.

Parameters:

Name Type Description Default
each Any

List accessor (or a plain list, which renders once).

required
children Callable[..., Any]

The row callback.

required
fallback Any

Rendered when the list is empty.

None
keyed bool | Callable[[Any], Any]

Matching strategy.

True
Example
# With a key function both arguments are accessors.
For(todos, lambda todo, i: li(lambda: todo()["title"]), keyed=lambda t: t["id"])

Repeat

Repeat(count: Any, children: Callable[[int], Any], fallback: Any = None, *, start: int | Any = 0) -> VNode

Render children(i) for i in range(start, start + count) with no diffing.

Rendering is driven purely by the count: growing mounts new tail slots, shrinking disposes them, and nothing else is touched. Use it for pagination dots, ratings, skeletons, and other count-driven UI.

Parameters:

Name Type Description Default
count Any

Count accessor or plain integer.

required
children Callable[[int], Any]

(index: int) -> VNode, rendered once per slot.

required
fallback Any

Rendered when the count is zero.

None
start int | Any

First index (accessor or int).

0
Example
Repeat(rating, lambda i: span("*"))

Switch

Switch(*matches: Match, fallback: Any = None) -> VNode

Render the first Match whose condition is truthy.

Conditions are evaluated in order inside the switch's own scope; only a change in which branch matches re-renders, so unrelated value changes are ignored (unless a branch is keyed).

Switch(
    Match(lambda: status() == "loading", lambda: p("Loading...")),
    Match(lambda: status() == "ready", lambda: p("Ready")),
    fallback=lambda: p("Unknown"),
)

dynamic

dynamic(source: Any) -> Callable[..., VNode]

Turn an accessor for a component (or tag) into a component you can call.

The returned callable behaves like any component: call it with children and props to get a VNode. Each instance re-mounts when source resolves to a different component; while source is an async computation with no value yet, the instance keeps its current content and the nearest Loading shows its fallback. Passing None renders nothing.

This is the counterpart of SolidJS 2.0's dynamic(). For a one-off use, call the result inline: dynamic(lambda: views[kind()])(title="Hi").

Parameters:

Name Type Description Default
source Any

An accessor returning a component, a tag name, or None; a plain component or tag is accepted too.

required
Example
Editor = dynamic(lambda: RichEditor if rich_mode() else PlainEditor)

def Page():
    return div(Editor(value=draft, on_change=set_draft))

client_only

client_only(children: Any, *, fallback: Any = None) -> VNode

Render children only in the browser, after hydration.

During a server render, and while the browser hydrates that render, fallback shows instead; the children replace it as soon as hydration has committed. In a page that wasn't server-rendered, the children render immediately. Use it for widgets that need browser APIs or whose output depends on the browser (a map, a chart measured from the viewport, the user's local time).

Parameters:

Name Type Description Default
children Any

A VNode or a zero-arg callable returning the content.

required
fallback Any

What the server renders, and what shows until hydration finishes.

None
Example
client_only(lambda: Chart(data=data), fallback=p("Loading chart..."))

is_hydrating

is_hydrating() -> bool

Whether the browser is adopting server-rendered DOM right now.

True only during the synchronous mount of hydrate: render what the server rendered, then switch to client-only values from on_settled. Always false on the server and in pages that weren't server-rendered.

NoHydration

NoHydration(*children: Any) -> VNode

Render children on the server as static HTML the browser won't hydrate.

While the browser hydrates, the region's server-rendered DOM is kept as is and nothing inside it is mounted, so it costs no Python work and never updates. In a page that wasn't server-rendered, the children render normally. Use it for content that never changes after the first paint: article bodies, footers, legal text.

Matches Solid 2.0's <NoHydration>.

Hydration

Hydration(*children: Any, id: str | None = None) -> VNode

Mark a subtree for hydration; a passthrough in Wybthon.

Solid 2.0 uses <Hydration> to re-enable hydration for an island inside <NoHydration>. Wybthon keeps a NoHydration region static as a whole while hydrating, so this renders its children unchanged. It's provided so code shared with Solid's model reads the same.

Parameters:

Name Type Description Default
*children Any

The content.

()
id str | None

Accepted for parity; unused.

None

What's in this module

Control-flow primitives that create isolated reactive scopes, so only the relevant subtree updates when a condition or list changes. Each is a function returning a component VNode; conditions and sources are accessors (or plain values), and children and fallback slots are VNodes or callables evaluated inside the primitive's own scope. All arguments are positional except the keyword-only options noted below.

Name Description
Show Show(when, children, fallback=None, *, keyed=False): render one branch by truthiness.
For For(each, children, fallback=None, *, keyed=True): one cached row per item; rows move, never re-diff.
Repeat Repeat(count, children, fallback=None, *, start=0): children(i) for i in range(start, start + count).
Switch / Match Switch(Match(when, children, keyed=False), ..., fallback=None): first truthy branch wins.
dynamic dynamic(source): a component whose implementation (a component or tag) is chosen by a reactive source. Call the result with children and props.
client_only client_only(children, *, fallback=None): render children only in the browser, after hydration; fallback renders on the server.
NoHydration NoHydration(*children): server-rendered HTML the browser keeps as static DOM instead of hydrating.
Hydration Hydration(*children, id=None): a passthrough kept for parity with Solid 2.0.
is_hydrating True only during the synchronous mount of hydrate.

For keying shapes

keyed selects how rows are matched between updates, and with it the callback shape:

keyed Rows match by children(item, index) receives
True (default) Identity (scalars by value) the raw item, Accessor[int]
False Position Accessor[T], int
key(item) callable The key; a new object with the same key updates the row in place Accessor[T], Accessor[int]

The row callback runs once per row inside the row's owner scope and untracked; anything reactive inside a row must read an accessor within a hole, memo, or effect. Passing a plain list for each renders once and warns in dev mode.

from wybthon import For, Match, Repeat, Show, Switch, create_signal, li, p, span, ul

todos, set_todos = create_signal([{"id": 1, "title": "Ship", "done": False}])
status, set_status = create_signal("ready")
rating, set_rating = create_signal(3)
user, set_user = create_signal(None)

view = ul(
    Show(user, lambda u: li("Hello, ", lambda: u()["name"]), fallback=li("Sign in")),
    For(todos, lambda todo, i: li(lambda: f"{i() + 1}. {todo['title']}")),
    For(todos, lambda todo, i: li(lambda: todo()["title"]), keyed=lambda t: t["id"]),
    li(Repeat(rating, lambda i: span("*"), start=1)),
    Switch(
        Match(lambda: status() == "loading", lambda: p("Loading...")),
        Match(lambda: status() == "ready", lambda: p("Ready")),
        fallback=lambda: p("Unknown"),
    ),
)
  • Show tracks only the truthiness of when; a callable children may take the value accessor (or the raw value with keyed=True, which re-creates the branch on every change).
  • Repeat is driven purely by the count: growing mounts tail slots, shrinking disposes them, and nothing else is touched. count and start may be accessors or ints.
  • dynamic accepts a tag name, a component, None, or an accessor returning one of those; each instance remounts when the resolved component changes.
  • client_only renders its fallback during a server render and while hydrating, then its children; see Server rendering.
  • NoHydration costs no Python work while hydrating: the kernel keeps the region's server DOM as is (CLAIM_STATIC) and nothing inside it mounts or updates. In a page that wasn't server-rendered, its children render normally.

See also