Effects
Effects run code in response to state changes—for logging, analytics, syncing with external systems, or any side effect that shouldn't happen during rendering.
What are effects?
An effect is a function that:
- Runs after state changes
- Automatically tracks which state it reads
- Re-runs when any tracked state changes
- Can return a cleanup function
Effects are for side effects—things that happen outside of rendering, like logging, API calls, timers, or subscriptions.
Effects in State classes
Use the @ps.effect decorator to define effects on your state:
class ToggleState(ps.State):
enabled: bool = False
@ps.effect
def log_changes(self):
print(f"Toggle is now: {self.enabled}")Pulse tracks what the effect reads. When self.enabled changes, the effect runs again automatically.
With cleanup
Effects can return a cleanup function. This is useful for timers, subscriptions, or anything that needs to be torn down:
class TimerState(ps.State):
count: int = 0
running: bool = False
@ps.effect
def start_timer(self):
if not self.running:
return
timer_id = set_interval(lambda: self.tick(), 1000)
def cleanup():
clear_interval(timer_id)
return cleanup
def tick(self):
self.count += 1Cleanup runs:
- Before the effect runs again (when tracked dependencies change)
- When the state is disposed
Effects in components
Use @ps.effect to define effects inside components. Pulse automatically registers them:
@ps.component
def OrderTracker():
with ps.init():
state = OrderState()
@ps.effect
def log_status_changes():
print(f"Order status: {state.status}")
analytics.track("order_status_changed", status=state.status)
return ps.div(f"Status: {state.status}")Effects defined with @ps.effect inside a component are automatically cached by their code location. You don't need to register them separately.
Conditional effects
Effects inside conditionals work as expected. They're created when the condition is true and disposed when it becomes false:
@ps.component
def ConditionalLogger():
with ps.init():
state = LoggerState()
if state.logging_enabled:
@ps.effect
def log_changes():
print(f"Value: {state.value}")
return ps.div(f"Value: {state.value}")Loop effects with keys
When using @ps.effect inside a loop, provide a unique key to disambiguate each effect:
@ps.component
def ItemTracker():
with ps.init():
state = ItemState()
for item in state.items:
@ps.effect(key=item.id) # Key makes each effect unique
def track_item(item=item): # Capture via default arg
print(f"Item {item.name}: count={item.count}")
return ps.ul(*[ps.li(item.name) for item in state.items])Without a key, Pulse raises an error because all effects would share the same code location.
Dynamic key effects
When the key changes, the old effect is disposed and a new one is created:
@ps.component
def UserTracker():
with ps.init():
user_id = ps.Signal("user-1")
@ps.effect(key=user_id()) # Effect recreated when user_id changes
def track_user():
print(f"Tracking user: {user_id()}")
def cleanup():
print(f"Stopped tracking: {user_id()}")
return cleanup
return ps.div(f"User: {user_id()}")Common mistakes
Using effects for derived data. Use computed values instead:
# Don't do this
@ps.effect
def update_visible(self):
self.visible_todos = [t for t in self.todos if not t.done]
# Do this instead
@ps.computed
def visible_todos(self):
return [t for t in self.todos if not t.done]Computeds are synchronous and cached. Effects run after rendering and can cause additional rerenders—leading to flickering or infinite loops.
Forgetting cleanup. If your effect creates timers, subscriptions, or event listeners, always return a cleanup function:
@ps.effect
def subscribe(self):
subscription = event_bus.subscribe(self.on_event)
def cleanup():
subscription.unsubscribe()
return cleanup # Don't forget this!See also
- State and Reactivity - the core state and reactivity system
- Tutorial: Hooks