Read-only Signals and Events
WebComPy's state primitives are cells: they hold current state and propagate changes, with an equality contract that suppresses same-value writes. When the state comes from outside your component — a window resize, a visibility change, a callback from non-WebComPy code — you want a value your UI can read reactively, but you do not want to hand out write access. use_readonly_signal gives you exactly that: a read-only signal whose only write path is the update function it returns.
This page covers the primitive and the two convenience composables for browser state events. If your data is a stream of occurrences (chat messages, ticks, WebSocket events) where every arrival matters, see Signals and Streams instead — signal equality would silently drop duplicates.
use_readonly_signal: an external-only write path
from webcompy import use_readonly_signal
view, update = use_readonly_signal(0)
# view: ReadonlySignal[int] — readable, but has no setter
# update(...) is the ONLY way to change it
- The returned signal is a
ReadonlySignal(from webcompy.signal import ReadonlySignalfor annotations): it exposes.valueand nothing else — noset_value, no value setter. updatemirrors the signal equality contract: passing a value equal to the current one does not notify consumers (a resized width that did not change stays silent).- It is context-free: call it in a standalone script, at module level, or inside another composable — no component setup required, no warning emitted.
- Values are client-side derived state: they never participate in hydration transfer, and SSR renders
initialunchanged.
Bridging an external callback:
from webcompy import use_readonly_signal
view, update = use_readonly_signal(0)
def on_pressure(value):
update(value)
pressure_sensor.subscribe(on_pressure)
use_window_event: window state events
use_window_event(event_type, initial, *, transform=None) bridges a window-level state event into a read-only signal. Call it inside a component's setup: the listener is attached immediately and automatically removed when the component is destroyed — no orphaned listener or browser proxy.
from webcompy import use_window_event
@define_component()
def EventPage(context):
width, _ = use_window_event("resize", 0, transform=lambda e: e.target.innerWidth)
return html.DIV({}, str(width.value))
transform: Callable[[Any], T] | Noneconverts the raw event into the signal's value type; when omitted, the raw event object is stored as-is.- An exception raised inside
transformis logged and swallowed — the signal keeps its previous value and the browser's event dispatch is not interrupted. - Inside component setup with a resolvable
HostPort, the listener is registered and unregistered onon_before_destroy. The composable occupies the component's single destroy-hook slot, so register your ownon_before_destroyhook before calling it — a hook registered later would overwrite the cleanup and leak the listener. - Outside component setup a
UserWarningis emitted and nothing is attached (leak-free). During SSR/SSG the server port is a no-op, so the page rendersinitialand hydration is unaffected.
use_document_event: document state events
use_document_event is the same composable for document-level events (e.g. visibilitychange, pointer state), registered through DOMPort.add_document_event_listener:
from webcompy import use_document_event
@define_component()
def EventPage(context):
hidden, _ = use_document_event(
"visibilitychange",
False,
transform=lambda e: bool(e.target.hidden),
)
return html.DIV({}, "hidden" if hidden.value else "visible")
Lifecycle and error semantics are identical to use_window_event.
State events, not occurrence events
Signal equality means update(v) where v equals the current value produces no notification. That is exactly right for state: the window did not change size, the tab did not change visibility. It is wrong for occurrence streams — a duplicate message or a repeated tick would be silently swallowed. For those, use to_reactive_list (accumulate every item) or to_async_iter (consume every update) from webcompy.aio, or a plain callable event handler.
Standalone usage
The composables deliberately refuse to attach listeners outside component setup. If you need a window/document listener at module level or in a script, register it yourself and pair it with the primitive:
from webcompy import use_readonly_signal
view, update = use_readonly_signal(0)
def on_resize(event):
update(event.target.innerWidth)
# browser-only setup; remove the listener yourself
host = ... # your HostPort (e.g. via inject(HOST_PORT_KEY) inside an app scope)
_remove = host.add_window_event_listener("resize", on_resize)