Usage Guide¶
This guide walks through common usage patterns with examples. For a complete reference, see the API docs.
Signals and Computed Values¶
Signal holds mutable state.
Computed represents derived state. It subscribes to dependencies and updates when they change.
Reading the underlying value (.value)¶
Signal and Computed display as <...> to indicate they are reactive objects.
Use .value when you need the plain Python value.
from signified import Signal
price = Signal(19.99)
quantity = Signal(2)
subtotal = price * quantity
print(subtotal) # <39.98> (reactive)
print(subtotal.value) # 39.98 (plain float)
quantity.value = 3
print(subtotal.value) # 59.97
Signal.value is read/write. Computed.value is read-only and updates from dependencies.
Computed from operators¶
from signified import Signal
price = Signal(19.99)
quantity = Signal(2)
subtotal = price * quantity
print(subtotal) # <39.98>
quantity.value = 3
print(subtotal) # <59.97>
Computed from functions (@computed)¶
from signified import Signal, computed
numbers = Signal([1, 2, 3, 4, 5])
@computed
def stats(nums):
return {
"sum": sum(nums),
"mean": sum(nums) / len(nums),
"min": min(nums),
"max": max(nums),
}
result = stats(numbers)
print(result) # <{'sum': 15, 'mean': 3.0, 'min': 1, 'max': 5}>
numbers.value = [2, 4, 6, 8, 10]
print(result) # <{'sum': 30, 'mean': 6.0, 'min': 2, 'max': 10}>
Composing Computed values¶
Computed values can be used as dependencies for other computed values.
from signified import Signal, computed
x = Signal(3)
x_squared = x**2
@computed
def plus_one(v):
return v + 1
y = plus_one(x_squared)
print(y) # <10>
x.value = 5
print(y) # <26>
This composition is the core pattern: define state once, derive the rest.
Attribute Access, Method Calls, and Assignment¶
You can reactively read attributes and call methods from objects inside signals.
from dataclasses import dataclass
from signified import Signal
@dataclass
class Person:
name: str
age: int
def greet(self):
return f"Hello, I'm {self.name} and I'm {self.age} years old!"
person = Signal(Person("Alice", 30))
name_display = person.name
greeting = person.greet()
print(name_display) # <"Alice">
print(greeting) # <"Hello, I'm Alice and I'm 30 years old!">
# __setattr__ support updates the wrapped object and notifies dependents
person.name = "Bob"
print(name_display) # <"Bob">
print(greeting) # <"Hello, I'm Bob and I'm 30 years old!">
Method chaining works too:
from signified import Signal
text = Signal(" Hello, World! ")
processed = text.strip().lower().replace(",", "")
print(processed) # <"hello world!">
text.value = " Goodbye, World! "
print(processed) # <"goodbye world!">
Collections and Item Assignment¶
Indexing is reactive, and __setitem__ can notify dependents for list and dict.
from signified import Signal, computed
numbers = Signal([1, 2, 3])
total = computed(sum)(numbers)
print(numbers[0]) # <1>
print(total) # <6>
numbers[0] = 9
print(total) # <14>
from signified import Signal
config = Signal({"theme": "dark", "font_size": 14})
theme = config["theme"]
print(theme) # <"dark">
config["theme"] = "light"
print(theme) # <"light">
Conditional Logic with where¶
where(a, b) picks a when the condition is truthy, otherwise b.
from signified import Signal, computed
username = Signal(None)
is_logged_in = username.rx.is_not(None)
@computed
def welcome(name):
return f"Welcome back, {name}!"
message = is_logged_in.rx.where(welcome(username), "Please log in")
print(message) # <"Please log in">
username.value = "admin"
print(message) # <"Welcome back, admin!">
Applying functions¶
map¶
map applies a function to a reactive value and returns a new Computed. It's a shorthand for computed(fn)(source) — the result re-evaluates whenever the source changes.
from signified import Signal
temperature_c = Signal(20)
temperature_f = temperature_c.rx.map(lambda c: (c * 9 / 5) + 32)
# equivalent to:
# temperature_f = computed(lambda c: (c * 9 / 5) + 32)(temperature_c)
print(temperature_f.value) # 68.0
temperature_c.value = 25
print(temperature_f.value) # 77.0
peek vs effect¶
Use peek when you want a side-effect to fire only when you explicitly read .value — useful for debugging or logging on demand. Use effect when you want a side-effect to fire automatically on every change.
peek is lazy like other Computed values: it only runs when .value is read.
effect is eager: it runs immediately and again on every source update.
from signified import Signal
price = Signal(10)
total = price.rx.map(lambda p: p * 1.2).rx.peek(lambda v: print("total:", v))
price.value = 10 # Nothing happens
price.value = 20 # Nothing happens
price.value = 30 # Nothing happens
price.value = 10 # Nothing happens
total.value # prints: 'total: 12.0'
price.value = 20 # Nothing happens
total.value # prints: 'total: 24.0'
Utility Helpers¶
unref makes functions work with either plain values or reactive values.
from signified import HasValue, Signal, unref
def process_data(value: HasValue[float]) -> float:
return unref(value) * 2
print(process_data(4)) # 8
print(process_data(Signal(5))) # 10
Related helpers:
deep_unref: recursively unwraps nested containers of reactive valuesas_rx: wraps plain values intoSignal(or returns the input reactive value)has_value: type guard for checkingHasValue[T]Signal.at(...): temporary scoped value override via context manager
Manual Invalidation¶
Most updates flow automatically through the reactive graph. Occasionally you may rewire dependencies through a non-reactive container (e.g. replacing an attribute on a plain Python object). In that case, call invalidate() to force a Computed to fully re-evaluate on next read.
from signified import Signal, computed
class Holder:
def __init__(self, sig):
self.sig = sig
holder = Holder(Signal(5))
derived = computed(lambda holder: holder.sig * 2)(holder)
print(derived.value) # 10
holder.sig = Signal(20) # non-reactive rewire — graph doesn't know
print(derived.value) # still 10
derived.invalidate()
print(derived.value) # 40