Advanced Features¶
This page covers advanced features and techniques in reaktiv for building more sophisticated reactive systems.
Custom Equality Functions¶
By default, reaktiv uses identity comparison (is) to determine if a signal's value has changed. For more complex types, you can provide custom equality functions:
from reaktiv import Signal
# Custom equality for dictionaries
def dict_equal(a, b):
if not isinstance(a, dict) or not isinstance(b, dict):
return a == b
if set(a.keys()) != set(b.keys()):
return False
return all(a[k] == b[k] for k in a)
# Create a signal with custom equality
user = Signal({"name": "Alice", "age": 30}, equal=dict_equal)
# This won't trigger updates because the dictionaries are equal by value
user.set({"name": "Alice", "age": 30})
# This will trigger updates because the "age" value is different
user.set({"name": "Alice", "age": 31})
Custom equality functions are especially useful for:
- Complex data structures like dictionaries, lists, or custom objects
- Case-insensitive string comparison
- Numerical comparison with tolerance (for floating-point values)
- Domain-specific equality (e.g., comparing users by ID regardless of other attributes)
Effect Cleanup¶
Effects can register cleanup functions that will run before the next execution or when the effect is disposed:
from reaktiv import Signal, Effect
counter = Signal(0)
def counter_effect(on_cleanup):
value = counter()
print(f"Setting up for counter value: {value}")
# Set up some resource or state
# Define cleanup function
def cleanup():
print(f"Cleaning up for counter value: {value}")
# Release resources, remove event listeners, etc.
# Register the cleanup function
on_cleanup(cleanup)
# Create and schedule the effect
logger = Effect(counter_effect)
# Prints: "Setting up for counter value: 0"
# Update the signal
counter.set(1)
# Prints: "Cleaning up for counter value: 0"
# Prints: "Setting up for counter value: 1"
# Dispose the effect
logger.dispose()
# Prints: "Cleaning up for counter value: 1"
This pattern is useful for:
- Managing subscriptions to external event sources
- Releasing resources when values change or the effect is disposed
- Setting up and tearing down UI elements in response to data changes
- Cancelling pending operations when new values arrive
Asynchronous Iteration¶
The to_async_iter utility lets you use signals with async for loops:
import asyncio
from reaktiv import Signal, to_async_iter
async def main():
counter = Signal(0)
# Start a task that increments the counter
async def increment_counter():
for i in range(1, 5):
await asyncio.sleep(1)
counter.set(i)
asyncio.create_task(increment_counter())
# Use the signal as an async iterator
async for value in to_async_iter(counter):
print(f"Got value: {value}")
if value >= 4:
break
asyncio.run(main())
Output:
This is useful for:
- Building reactive data processing pipelines
- Integrating with other async code
- Responding to signal changes in event loops
- Creating reactive streams of data
Selective Dependency Tracking¶
You can selectively control which signals create dependencies using untracked:
from reaktiv import Signal, Effect, untracked
user_id = Signal(123)
user_data = Signal({"name": "Alice"})
show_details = Signal(False)
def render_user():
# Always creates a dependency on user_id
id_value = user_id()
# Only access user_data if show_details is true,
# but don't create a dependency on show_details
if untracked(show_details):
print(f"User {id_value}: {user_data()}")
else:
print(f"User {id_value}")
# Create and schedule the effect
display = Effect(render_user)
# Update dependencies will trigger the effect
user_id.set(456)
# This update won't trigger the effect, even though it changes the output
show_details.set(True)