@reactive¶
@reactive subscribes a function to one or more observables. It runs once immediately with the current value(s), then again on every subsequent change, until .unsubscribe() is called.
Basic Usage¶
from fynx import reactive, observable
count = observable(0)
@reactive(count)
def log_count(value):
print(f"Count: {value}")
# Prints immediately: "Count: 0"
count.set(5) # Prints: "Count: 5"
count.set(10) # Prints: "Count: 10"
Decorator Forms¶
@reactive accepts a Store class, a single observable, or multiple observables:
class UserStore(Store):
name = observable("Alice")
age = observable(30)
# Store: the function receives a StoreSnapshot
@reactive(UserStore)
def on_user_change(snapshot):
print(snapshot.name, snapshot.age)
# Single observable: the function receives its value
@reactive(UserStore.name)
def on_name_change(name):
print(name)
# Multiple observables: one argument per observable
@reactive(UserStore.name, UserStore.age)
def on_either_change(name, age):
print(name, age)
Execution Timing¶
@reactive fires immediately on decoration whenever it can, then again on every later qualifying change:
- Store targets fire immediately with a
StoreSnapshotof the current state. - Observable, computed, or merged targets fire immediately with the current value.
- Conditional targets (built with
@/.requiring()) fire immediately only if the gate is already active at decoration time; otherwise the function waits until the gate opens.
ready = observable(True)
@reactive(ready)
def on_ready(value):
print(f"Ready: {value}")
# Prints immediately: "Ready: True"
ready.set(False) # Prints: "Ready: False"
Manual Calls Are Blocked While Subscribed¶
@reactive(count)
def log_count(value):
print(f"Count: {value}")
log_count(10) # Raises fynx.reactive.ReactiveFunctionWasCalled
.unsubscribe() releases the function back to normal, callable behavior:
Self-Mutation Raises, It Doesn't Loop¶
A reaction that sets the observable it watches doesn't run forever. The decoration itself runs once immediately, before the subscription is registered, so that first call succeeds quietly - but any later external change raises:
count = observable(0)
@reactive(count)
def increment_forever(value):
count.set(value + 1)
count.set(5)
# Raises RuntimeError: Circular dependency detected in reactive computation!
Store-Level Reactions¶
Reacting to a Store class receives a snapshot rather than individual values, and fires on any attribute change:
class UserStore(Store):
name = observable("Alice")
age = observable(30)
@reactive(UserStore)
def sync_to_server(snapshot):
api.post('/user/update', {'name': snapshot.name, 'age': snapshot.age})
UserStore.name = "Bob" # Triggers sync_to_server
UserStore.age = 31 # Also triggers sync_to_server
Key Properties¶
- Eager: Fires immediately on decoration when its target is active, then again on every later qualifying change
- Exclusive: Raises
ReactiveFunctionWasCalledif called manually while still subscribed - Reversible:
.unsubscribe()returns the function to normal, callable behavior - Side-effect-only by convention: use
.then()/.alongside()/.all()/.requiring()to derive values; reserve@reactivefor effects that leave the reactive graph (I/O, logging, UI updates)
See Using @reactive for the full walkthrough and Best Practices for anti-patterns and gotchas.
ReactiveFunctionWasCalled ¶
Raised when a reactive function is called manually instead of through reactive triggers.
A reactive function is meant to run only when its observable dependencies
change, not be called directly. Modify the observables that trigger it
instead, or call .unsubscribe() to turn it back into a plain callable.
ReactiveWrapper ¶
Wraps a reactive function and manages its subscription lifecycle.
While subscribed, the function runs automatically when its targets
change, and calling it directly raises ReactiveFunctionWasCalled. After
unsubscribe(), it reverts to a plain callable. The wrapper preserves
function metadata (name, docstring) and tracks subscriptions internally.
Initialize the wrapper with the function and its reactive targets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
func
|
Callable[P, R]
|
The original function to wrap |
required |
targets
|
tuple[Any, ...]
|
Tuple of observables/stores to react to |
required |
__call__ ¶
Call the wrapped function, raising an error if still subscribed.
While subscribed, manual calls raise ReactiveFunctionWasCalled,
since the function is meant to run only when its dependencies change.
After unsubscribe(), this delegates to the original function normally.
unsubscribe ¶
Unsubscribe from all reactive targets, making this a normal function again.
Idempotent - calling it multiple times is safe. Afterward, the
function stops responding to changes and can be called manually
without raising ReactiveFunctionWasCalled.
reactive ¶
reactive(
target: type[Store],
) -> Callable[
[Callable[[StoreSnapshot], R]], ReactiveWrapper[[StoreSnapshot], R]
]
reactive(
first: ObservableOperand[A], second: ObservableOperand[B]
) -> Callable[[Callable[[A, B], R]], ReactiveWrapper[[A, B], R]]
reactive(
first: ObservableOperand[A],
second: ObservableOperand[B],
third: ObservableOperand[C],
) -> Callable[[Callable[[A, B, C], R]], ReactiveWrapper[[A, B, C], R]]
reactive(
first: ObservableOperand[A],
second: ObservableOperand[B],
third: ObservableOperand[C],
fourth: ObservableOperand[D],
) -> Callable[[Callable[[A, B, C, D], R]], ReactiveWrapper[[A, B, C, D], R]]
reactive(
first: ObservableOperand[A],
second: ObservableOperand[B],
third: ObservableOperand[C],
fourth: ObservableOperand[D],
fifth: ObservableOperand[E],
) -> Callable[
[Callable[[A, B, C, D, E], R]], ReactiveWrapper[[A, B, C, D, E], R]
]
reactive(
first: ObservableOperand[A],
second: ObservableOperand[B],
third: ObservableOperand[C],
fourth: ObservableOperand[D],
fifth: ObservableOperand[E],
sixth: ObservableOperand[F],
) -> Callable[
[Callable[[A, B, C, D, E, F], R]], ReactiveWrapper[[A, B, C, D, E, F], R]
]
reactive(
first: ObservableOperand[A],
second: ObservableOperand[B],
third: ObservableOperand[C],
fourth: ObservableOperand[D],
fifth: ObservableOperand[E],
sixth: ObservableOperand[F],
seventh: ObservableOperand[G],
) -> Callable[
[Callable[[A, B, C, D, E, F, G], R]],
ReactiveWrapper[[A, B, C, D, E, F, G], R],
]
reactive(
first: ObservableOperand[A],
second: ObservableOperand[B],
third: ObservableOperand[C],
fourth: ObservableOperand[D],
fifth: ObservableOperand[E],
sixth: ObservableOperand[F],
seventh: ObservableOperand[G],
eighth: ObservableOperand[H],
) -> Callable[
[Callable[[A, B, C, D, E, F, G, H], R]],
ReactiveWrapper[[A, B, C, D, E, F, G, H], R],
]
reactive(
first: ObservableOperand[A],
second: ObservableOperand[B],
third: ObservableOperand[C],
fourth: ObservableOperand[D],
fifth: ObservableOperand[E],
sixth: ObservableOperand[F],
seventh: ObservableOperand[G],
eighth: ObservableOperand[H],
ninth: ObservableOperand[I],
) -> Callable[
[Callable[[A, B, C, D, E, F, G, H, I], R]],
ReactiveWrapper[[A, B, C, D, E, F, G, H, I], R],
]
reactive(
first: ObservableOperand[A],
second: ObservableOperand[B],
third: ObservableOperand[C],
fourth: ObservableOperand[D],
fifth: ObservableOperand[E],
sixth: ObservableOperand[F],
seventh: ObservableOperand[G],
eighth: ObservableOperand[H],
ninth: ObservableOperand[I],
tenth: ObservableOperand[J],
) -> Callable[
[Callable[[A, B, C, D, E, F, G, H, I, J], R]],
ReactiveWrapper[[A, B, C, D, E, F, G, H, I, J], R],
]
Create a reactive handler that works as a decorator.
Declare which observables the function cares about and FynX handles subscribing and unsubscribing for you.
The decorator accepts three patterns:
-
Store subscription:
@reactive(StoreClass)reacts to all observables in the store, passing aStoreSnapshotto the function. -
Single observable:
@reactive(observable)reacts to one observable, passing its current value to the function. -
Multiple observables:
@reactive(obs1, obs2, ...)merges observables and passes their values as separate arguments.
The function executes immediately with current values when decorated, then
runs automatically whenever dependencies change. While subscribed, manual
calls raise ReactiveFunctionWasCalled. Call .unsubscribe() to restore
normal function behavior.
Examples:
from fynx import observable, reactive, Store
# Single observable
count = observable(0)
@reactive(count)
def log_count(value):
print(f"Count: {value}")
count.set(5) # Prints: "Count: 5"
# Store subscription
class UserStore(Store):
name = observable("Alice")
age = observable(30)
@reactive(UserStore)
def on_user_change(snapshot):
print(f"User: {snapshot.name}, Age: {snapshot.age}")
UserStore.name = "Bob" # Triggers on_user_change
# Multiple observables
@reactive(UserStore.name, UserStore.age)
def on_name_or_age(name, age):
print(f"Name: {name}, Age: {age}")
UserStore.age = 31 # Triggers on_name_or_age
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*targets
|
Any
|
Store class, Observable instance(s), or multiple Observable instances |
()
|
Returns:
| Type | Description |
|---|---|
Any
|
ReactiveWrapper instance that acts like the original function but prevents |
Any
|
manual calls while subscribed. |