Authoring Patterns
Authoring Patterns¶
This guide shows how to author components in Wybthon using both function and class styles. It focuses on props, state with signal/computed/effect, children composition, cleanup, and context.
Function components¶
Use plain Python callables that receive a props dict and return a VNode via h(...).
from wybthon import h
def Hello(props):
name = props.get("name", "world")
return h("div", {"class": "hello"}, f"Hello, {name}")
- Props are read-only; compute derived values inline or via
computedwhen expensive. - To accept children, read
props.get("children", []). The VDOM passes children via props for components.
Children handling:
def Card(props):
title = props.get("title", "")
children = props.get("children", [])
return h("section", {"class": "card"}, h("h3", {}, title), children)
Side effects and subscriptions are typically modeled in class components (see below). Function components are best for presentational/stateless pieces.
Class components¶
Subclass Component to encapsulate state and lifecycles.
from wybthon import Component, h, signal, effect, on_effect_cleanup
class Counter(Component):
def __init__(self, props):
super().__init__(props)
self.count = signal(0)
def inc(_evt):
self.count.set(self.count.get() + 1)
self._inc = inc
# Example: reactive side effect
comp = effect(lambda: print("count:", self.count.get()))
# Ensure cleanup on unmount
on_effect_cleanup(comp, lambda: print("effect disposed"))
self.on_cleanup(lambda: comp.dispose())
def render(self):
return h(
"div",
{"class": "counter"},
h("p", {}, f"Count: {self.count.get()}"),
h("button", {"on_click": getattr(self, "_inc", lambda e: None)}, "Increment"),
)
- State: store
signalinstances as attributes. Read with.get()during render. - Events: pass bound methods or closures via
on_click,on_input, etc. - Cleanup: register teardown work with
on_cleanup(fn)so it runs on unmount. - Updates:
on_update(prev_props)fires after a prop change and diff is applied.
Props and defaults¶
Prefer props.get("key", default) when reading optional values. For required props, consider simple guards at the top of render.
Passing and using children¶
The VDOM passes children via props["children"] for component tags. Normalize to a list when rendering:
def Layout(props):
children = props.get("children", [])
if not isinstance(children, list):
children = [children]
return h("div", {"class": "layout"}, children)
Context¶
Provide values with Provider and read with use_context.
from wybthon import Provider, h, use_context
from wybthon.context import create_context
Theme = create_context("light")
def ThemeLabel(_props):
return h("span", {}, f"Theme: {use_context(Theme)}")
def Layout(props):
children = props.get("children", [])
return h("div", {}, h(Provider, {"context": Theme, "value": "dark"}, children))
Choosing between function and class¶
- Use function components for pure UI composition without local reactive state or lifecycle needs.
- Use class components when you need reactive state, effects, cleanup, or lifecycle hooks.
Both interoperate seamlessly and can be composed together.
Patterns checklist¶
- Read props defensively with defaults.
- Store signals on
selfin class components; avoid re-creating them duringrender. - Use
effectfor side-effects; dispose inon_cleanup. - Accept
childrenand pass them through when building layout components. - Keep events simple and avoid catching errors unless you can handle them.
Larger examples¶
The following end-to-end snippets demonstrate common authoring patterns with state, children composition, and cleanup.
1) Composition via children (function component)
from wybthon import h
def Card(props):
title = props.get("title", "")
children = props.get("children", [])
if not isinstance(children, list):
children = [children]
return h("section", {"class": "card"}, h("h3", {}, title), children)
def Page(_props):
return h(
"div",
{},
h(Card, {"title": "Composition"}, h("p", {}, "Card body via children")),
)
2) State and derived values (class component with signal + computed)
from wybthon import Component, h, signal, computed
class NamesList(Component):
def __init__(self, props):
super().__init__(props)
self.names = signal([])
self.starts_with_a = computed(
lambda: len([n for n in self.names.get() if str(n).lower().startswith("a")])
)
def make_add(name):
return lambda _evt: self.names.set(self.names.get() + [name])
def clear(_evt):
self.names.set([])
self._add_ada = make_add("Ada")
self._add_alan = make_add("Alan")
self._clear = clear
def render(self):
items = [h("li", {}, n) for n in self.names.get()]
return h(
"div",
{},
h("p", {}, f"Total: {len(self.names.get())} | Starts with A: {self.starts_with_a.get()}"),
h("div", {},
h("button", {"on_click": getattr(self, "_add_ada", lambda e: None)}, "+ Ada"),
h("button", {"on_click": getattr(self, "_add_alan", lambda e: None)}, "+ Alan"),
h("button", {"on_click": getattr(self, "_clear", lambda e: None)}, "Clear"),
),
h("ul", {}, items),
)
3) Cleanup and lifecycles (class component with on_cleanup)
from wybthon import Component, h, signal
class Timer(Component):
def __init__(self, props):
super().__init__(props)
self.seconds = signal(0)
# Create a JS interval and clean it up on unmount
try:
from js import setInterval, clearInterval
from pyodide.ffi import create_proxy
def tick():
self.seconds.set(self.seconds.get() + 1)
tick_proxy = create_proxy(lambda: tick())
interval_id = setInterval(tick_proxy, 1000)
def cleanup():
try:
clearInterval(interval_id)
except Exception:
pass
try:
tick_proxy.destroy()
except Exception:
pass
self.on_cleanup(cleanup)
except Exception:
# Non-browser or setup failure: no interval, still renders static value
pass
def render(self):
return h("div", {"class": "timer"}, f"Seconds: {self.seconds.get()}")
Putting it together:
from wybthon import h
def Card(props):
title = props.get("title", "")
children = props.get("children", [])
if not isinstance(children, list):
children = [children]
return h("section", {"class": "card"}, h("h3", {}, title), children)
def Page(_props):
return h(
"div",
{},
h(Card, {"title": "State & Derived"}, h(NamesList, {})),
h(Card, {"title": "Cleanup"}, h(Timer, {})),
)
See the demo app "Patterns" page for a working version of these examples.