Skip to content

VDOM

wybthon.vdom

vdom

Virtual DOM primitives, diffing, and rendering to real DOM elements.

This module is a thin compatibility shim that re-exports the public API from the focused sub-modules. Existing code that imports from wybthon.vdom continues to work unchanged.

Re-exports:

Classes:

Name Description
VNode

Virtual node representing an element, text, component, or reactive hole.

Functions:

Name Description
ErrorBoundary

Catch render errors in children and display a fallback.

create_portal

Render children into a different DOM container.

is_event_prop

Return True if name looks like an event handler prop.

mount

Mount a VNode (or string) into container, returning its DOM element.

patch

Diff old against new and apply minimal DOM changes inside container.

render

Render a VNode tree into a container element.

unmount

Unmount vnode, disposing its effects, ownership scope, and DOM.

Suspense

Render a fallback while one or more resources are loading.

Fragment

Group multiple children without adding an extra DOM wrapper element.

dynamic

Create a reactive-hole VNode that re-evaluates getter on dependency changes.

h

Create a VNode from a tag, props, and children.

is_getter

Return True when value is a zero-arg callable suitable for a reactive hole.

memo

Wrap a function component to skip re-mounts when its props are unchanged.

VNode

VNode(tag: Optional[Union[str, Callable[..., Any]]], props: Optional[PropsDict] = None, children: Optional[List[ChildType]] = None, key: Optional[Union[str, int]] = None)

Virtual node representing an element, text, component, or reactive hole.

Uses __slots__ for a compact memory layout and faster attribute access, which is meaningful when authoring large lists. Internal attributes (el, subtree, render_effect, component_ctx, _frag_end) are populated by the reconciler when the VNode is mounted.

Attributes:

Name Type Description
tag

Element tag name ("div"), special tag ("_text", "_dynamic", "_fragment"), or component callable.

props

Mapping of prop names to values. Event handlers, attributes, and reactive accessors all live here.

children

List of child VNode instances (or strings, before normalization).

key

Optional stable identity used for keyed list reconciliation.

ErrorBoundary

ErrorBoundary(props: Any) -> Any

Catch render errors in children and display a fallback.

Parameters:

Name Type Description Default
props Any

The component's props with the following keys:

  • fallback: A VNode, a string, or a callable (error, reset) -> VNode. The callable form may also accept just (error,).
  • on_error: Optional callback invoked with the caught exception (errors raised inside the callback are swallowed).
  • reset_key / reset_keys: When this value (or callable result) changes, the boundary auto-clears the current error.
  • children: Children rendered when no error is active.
required

Returns:

Type Description
Any

A reactive VNode subtree that swaps to the

Any

fallback whenever a child raises.

create_portal

create_portal(children: Union[VNode, List[VNode]], container: Any) -> VNode

Render children into a different DOM container.

Parameters:

Name Type Description Default
children Union[VNode, List[VNode]]

A single VNode or a list of them.

required
container Any

An Element instance or a CSS selector string identifying the target DOM container.

required

Returns:

Type Description
VNode

A VNode that, when mounted, mounts children into

VNode

container while remaining linked to the surrounding

VNode

component's reactive scope (signals, context, and lifecycle

VNode

hooks still apply).

is_event_prop

is_event_prop(name: str) -> bool

Return True if name looks like an event handler prop.

Both on_click (snake-case) and onClick (camelCase) styles are recognised.

Parameters:

Name Type Description Default
name str

Prop name to inspect.

required

Returns:

Type Description
bool

True for event handler props.

mount

mount(vnode: Union[VNode, str], container: Element, anchor: Any = None) -> Element

Mount a VNode (or string) into container, returning its DOM element.

Parameters:

Name Type Description Default
vnode Union[VNode, str]

The VNode to mount. Strings are coerced to text VNodes.

required
container Element

The parent element wrapper.

required
anchor Any

Optional sibling DOM node to insert before. When None, the new element is appended to container.

None

Returns:

Type Description
Element

The mounted Element wrapper.

patch

patch(old: Optional[VNode], new: VNode, container: Element) -> None

Diff old against new and apply minimal DOM changes inside container.

Same-type VNodes are patched in place (props and children diffed); different types are unmounted and remounted at the same anchor.

Parameters:

Name Type Description Default
old Optional[VNode]

The previously-rendered VNode, or None for the initial mount.

required
new VNode

The new VNode to render.

required
container Element

The parent element wrapper.

required

render

render(vnode: VNode, container: Union[Element, str]) -> Element

Render a VNode tree into a container element.

Subsequent calls with the same container patch the existing tree in place; only the differences are applied. Pass None (via the internal API) to unmount.

Parameters:

Name Type Description Default
vnode VNode

The root VNode to render.

required
container Union[Element, str]

An Element wrapper or a CSS selector string identifying an existing DOM node.

required

Returns:

Type Description
Element

The wrapped container Element. Useful for chaining or for

Element

retaining a reference to the mount point.

Example
from wybthon import h, render

render(h("h1", {}, "Hello, world!"), "#app")

unmount

unmount(vnode: VNode) -> None

Unmount vnode, disposing its effects, ownership scope, and DOM.

Calls cleanup on the owning component context (if any), removes delegated event handlers, runs on_cleanup callbacks, and detaches the underlying DOM node from its parent.

Parameters:

Name Type Description Default
vnode VNode

The VNode to tear down. Safe to call on already-unmounted nodes (becomes a no-op).

required

Suspense

Suspense(props: Any) -> Any

Render a fallback while one or more resources are loading.

Parameters:

Name Type Description Default
props Any

The component's props with the following keys:

  • resources / resource: A single Resource or a list of resources. When omitted, the component just renders its children.
  • fallback: VNode, string, or callable returning one of those. Shown while any resource is loading.
  • keep_previous (bool, default False): When True, show previously-resolved children during refetches instead of replacing them with the fallback.
  • children: Children rendered when no resource is loading.
required

Returns:

Type Description
Any

A reactive VNode subtree that toggles

Any

between fallback and children.

Fragment

Fragment(*args: Any) -> VNode

Group multiple children without adding an extra DOM wrapper element.

Fragments use empty comment nodes as start/end markers and mount their children directly into the parent container. This avoids extra elements that would pollute selectors like :first-child or affect layout.

Parameters:

Name Type Description Default
*args Any

Either a sequence of children (Fragment(a, b, c)) or a single dict containing a children key (the form used when Fragment is called as h(Fragment, {}, a, b, c)).

()

Returns:

Type Description
VNode

A _fragment VNode that the reconciler will mount inline.

Example
Fragment(h1("Title"), p("Body text"))
h(Fragment, {}, h1("Title"), p("Body text"))  # same thing

dynamic

dynamic(getter: Callable[[], Any], *, key: Optional[Union[str, int]] = None) -> VNode

Create a reactive-hole VNode that re-evaluates getter on dependency changes.

This is the explicit form of the same machinery that wraps callable children automatically. Use it when you want to be explicit about which child is dynamic, or to attach a stable key for keyed reuse inside a fragment.

The getter may return a VNode, a str, a list of either, or None.

Parameters:

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

Zero-arg callable evaluated inside its own effect. Any signal reads inside the getter become dependencies that trigger re-evaluation.

required
key Optional[Union[str, int]]

Optional stable identity used by keyed reconciliation.

None

Returns:

Type Description
VNode

A _dynamic VNode that the reconciler will mount as a reactive hole.

Example
div(dynamic(lambda: f"Hello, {name()}!"))

h

h(tag: Optional[Union[str, Callable[..., Any]]], props: Optional[PropsDict] = None, *children: Any) -> VNode

Create a VNode from a tag, props, and children.

This is the low-level VNode constructor used everywhere. For common HTML tags, prefer the helpers in wybthon.html (div, span, button, …).

Callable children (zero-argument getters) are passed through unchanged; normalize_children wraps them as _dynamic VNodes when the parent element mounts. Components receive their children verbatim via the children prop so they can decide how to render them.

Parameters:

Name Type Description Default
tag Optional[Union[str, Callable[..., Any]]]

An HTML tag name ("div"), a special tag ("_text", "_dynamic", "_fragment"), or a component callable.

required
props Optional[PropsDict]

Mapping of prop names to values. May be None.

None
*children Any

Children to attach. Lists/tuples are flattened.

()

Returns:

Type Description
VNode

A new VNode.

Example
from wybthon import h

view = h("button", {"on_click": handle_click}, "Click me")

is_getter

is_getter(value: Any) -> bool

Return True when value is a zero-arg callable suitable for a reactive hole.

The check excludes:

  • VNode instances
  • Classes (isinstance(value, type))
  • Components and providers (marked with _wyb_component / _wyb_provider)
  • Ref objects (have a current attribute)
  • Callables that require positional arguments (e.g. event handlers taking an event object)

Parameters:

Name Type Description Default
value Any

Any value, typically a child or prop value being normalized.

required

Returns:

Type Description
bool

True if the value should be treated as a reactive getter.

memo

memo(component: Callable[..., Any], are_props_equal: Optional[Callable[[PropsDict, PropsDict], bool]] = None) -> Callable[..., Any]

Wrap a function component to skip re-mounts when its props are unchanged.

Because Wybthon component bodies run once, memo is only useful when you want to skip re-mounting on a prop change (for example, components with an expensive setup phase). Most ordinary components do not need it; fine-grained holes already minimise DOM work.

Parameters:

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

The component callable to memoize.

required
are_props_equal Optional[Callable[[PropsDict, PropsDict], bool]]

Optional (old_props, new_props) -> bool comparator. Defaults to a shallow identity check (is) across every key.

None

Returns:

Type Description
Callable[..., Any]

A wrapped component callable. Identity is preserved across

Callable[..., Any]

renders so the reconciler can detect "same component, same

Callable[..., Any]

props".

Example
MemoList = memo(ExpensiveList,
                are_props_equal=lambda a, b: a["items"] == b["items"])

The VDOM system is implemented as a set of focused sub-modules. The wybthon.vdom module re-exports all public names for convenience:

Module Responsibility
wybthon.vnode VNode, h(), Fragment, memo()
wybthon.reconciler render(), mount(), unmount(), patch()
wybthon.props DOM prop application, style/event/dataset diffing
wybthon.error_boundary ErrorBoundary component
wybthon.suspense Suspense component
wybthon.portal create_portal()

You can import from either wybthon.vdom or the specific sub-module:

from wybthon.vdom import h, render          # re-export hub
from wybthon.vnode import h                 # direct import
from wybthon.reconciler import render       # direct import

Public API

  • VNode
  • h(tag, props=None, *children) -> VNode
  • Fragment(*children). Groups children without a wrapper element; the reconciler uses invisible comment markers and mounts children in the parent (no display:contents span).
  • render(vnode, container) -> Element
  • dynamic(getter, *, key=None) -> VNode. Wrap a zero-arg callable as a reactive hole (see Primitives, Reactive Holes). Callable children passed to h() are auto-wrapped via this helper.
  • is_getter(value) -> bool. Predicate the reconciler uses to decide whether a callable child or prop value is a reactive hole. Useful when authoring components or helpers that need to detect getters explicitly.
  • ErrorBoundary component
  • Suspense component
  • memo(component, are_props_equal=None). Memoize a function component.
  • create_portal(children, container). Render children into a different DOM container.

Keyed children and diffing

  • h(tag, {"key": key}, ...) assigns a stable identity to a child.
  • During reconciliation, children are matched by key first, then by type for unkeyed nodes.
  • Reorders are applied with minimal DOM moves using a right-to-left pass with a moving anchor.
  • Unmatched old nodes are unmounted; unmatched new nodes are mounted at the correct anchor.

Text nodes (fast-path)

  • When both the old and new nodes are text, the same DOM text node is reused and only nodeValue is updated.
  • Lists of plain text children are reconciled efficiently; typically the framework updates text in-place and minimizes DOM moves.
  • Example:
from wybthon import h, render
from wybthon.dom import Element

root = Element(node=document.createElement("div"))
render(h("div", {}, "hello"), root)
render(h("div", {}, "world"), root)  # updates the same text node

Suspense

Suspense renders a fallback while one or more resources are loading.

  • Props:
  • resource or resources=[...]
  • fallback – VNode/str/callable
  • keep_previous=False – keep children visible during subsequent reloads

ErrorBoundary

ErrorBoundary catches render errors from its subtree and renders a fallback.

  • Props:
  • fallback – VNode/str/callable. When callable, it is invoked as fallback(error, reset); if the callable only accepts one argument, it is invoked as fallback(error).
  • on_error – optional callback called with the thrown error when the boundary captures it.
  • reset_key – any value; when this value changes, the boundary automatically resets (clears the error) on the next render.
  • reset_keys – list/tuple of values; when the tuple of values changes, the boundary automatically resets.

  • Methods:

  • reset() – imperative method to clear the current error and attempt re-rendering children.

  • Notes:

  • If the fallback callable throws, a simple text node "Error rendering fallback" is shown.
  • When not in an error state, the boundary renders its children wrapped in a Fragment.

Prop semantics (style, dataset, value, checked)

  • style: pass a dict of CSS properties using camelCase keys. Keys are converted to kebab-case and applied via style.setProperty. On updates, keys that are absent in the new dict are removed with style.removeProperty. Passing None or a non-dict clears previously set style keys.
h("div", {"style": {"backgroundColor": "red", "fontSize": 14}})
# → sets background-color: red; font-size: 14
  • dataset: pass a dict; entries map to data-* attributes. On updates, keys not present are removed. Passing None or a non-dict clears previously set data-* attributes.
h("div", {"dataset": {"id": "x", "role": "button"}})
# → sets data-id="x" data-role="button"
  • value: for form controls, the DOM value property is set (falling back to the value attribute if needed). None becomes "". Removing the value prop resets it to "".

  • checked: for checkboxes/radios, the DOM checked property is set when available (falling back to the checked attribute). Removing the checked prop clears it to False.

memo

memo(component, are_props_equal=None) wraps a function component to skip re-mounting when props are unchanged.

  • By default, uses shallow identity comparison (is) on each prop value.
  • Pass a custom are_props_equal(old_props, new_props) -> bool for deeper comparison.
from wybthon import component, memo, p

@component
def ExpensiveList(items=None):
    its = items() or []
    return p("Items: ", str(len(its)))

MemoList = memo(ExpensiveList)

Because Wybthon components run once and then update via reactive holes, memo is only useful when you want to skip re-mounting on prop changes (for example, an expensive setup phase). Most components do not need it.

create_portal

create_portal(children, container) renders children into a different DOM container.

  • children: a single VNode or a list of VNodes.
  • container: an Element or CSS selector string.
from wybthon import create_portal, h

portal = create_portal(h("div", {}, "Modal content"), "#modal-root")

Development mode

Wybthon includes a development mode (DEV_MODE = True by default) that provides clear error messages to stderr when something goes wrong during rendering, event handling, or lifecycle hooks. Errors include component names and tracebacks.

from wybthon import set_dev_mode

set_dev_mode(False)  # disable for production