Signals#

A Signal is a reactive value. Bind one to a widget and the two stay in sync automatically: when the user edits the widget the signal updates, and when you call signal.set(...) the widget redraws. Signals are how you connect application state to the interface without wiring up change callbacks by hand.

Creating a signal#

Construct a Signal with its initial value. The value’s type is fixed at creation — passing a string makes a text signal, an int makes a numeric one, a bool makes a boolean one:

import bootstack as bs

with bs.App() as app:
    name = bs.Signal("World")     # text signal
    count = bs.Signal(0)          # integer signal
    enabled = bs.Signal(True)     # boolean signal
app.run()

Note

A signal must be created inside a running App. Creating one at module level — before bs.App() exists — raises an error, because a signal is backed by the application’s variable system.

Reading and writing#

Call the signal to read its current value. Use set() to update it:

name()              # "World"  — call to read
name.set("Universe")

set() enforces the signal’s type. Assigning a value of a different type raises TypeError — a Signal(0) accepts set(5) but rejects set(1.5). The one exception is numeric widening: a float signal also accepts an int (bs.Signal(0.0).set(5) stores 5.0). Setting the same value the signal already holds is a no-op and does not notify subscribers.

Binding to widgets#

Pass a signal to a widget to create a two-way binding. Text-bearing widgets use textsignal=; boolean and numeric widgets use signal=:

with bs.App(gap=8) as app:
    name = bs.Signal("World")

    bs.TextField(textsignal=name)
    bs.Label(textsignal=name)         # mirrors the field as you type

    bs.Button("Greet", on_click=lambda: print(f"Hello, {name()}!"))
app.run()

Typing in the field updates name; calling name.set(...) updates the field. The same signal can drive several widgets at once, keeping them all consistent.

See also

textsignal= is for widgets that carry text (TextField, TextArea). signal= is for boolean and numeric widgets (Checkbox, Slider, Switch).

Reacting to changes#

Subscribe a callback to run whenever the value changes. The callback receives the new value:

sub = count.subscribe(lambda value: print(f"count is now {value}"))

count.set(1)        # prints "count is now 1"

count.unsubscribe(sub)      # stop listening
count.unsubscribe_all()     # drop every subscriber

Pass immediate=True to fire the callback once with the current value at subscription time, in addition to future changes:

count.subscribe(update_total, immediate=True)

Derived signals#

map() returns a new, read-only signal whose value is computed from the source. It recomputes automatically whenever the source changes:

name = bs.Signal("world")
shout = name.map(str.upper)

shout()             # "WORLD"
name.set("hello")
shout()             # "HELLO"

Note

A derived signal is held weakly by its source. Keep a reference to it — assign it to a variable or bind it to a widget. If it is garbage-collected it silently stops updating.

See also#

  • Events — the subscribe / Stream model for widget events.

  • TextField — an input widget that accepts textsignal= / signal=.

API reference#

The complete reference — every method on Signal — lives on the Reactivity API page (Signal is part of the top-level compose surface). At a glance:

Signal

A reactive value that widgets can bind to.