State Propagation and Parent Notification
ObjectState provides mechanisms for propagating state changes from child states to parent states, enabling hierarchical UI updates and cross-component notifications.
Overview
In complex UI hierarchies with nested ObjectState instances (e.g., a step state containing function states), changes in child states often need to notify parent states. The state propagation system provides:
Parent notification: Child states can forward changes to parents
Reentrancy protection: Prevents infinite loops during propagation
Automatic field detection: Infers the parent field from scope hierarchy
Callback triggering: Fires parent on_resolved_changed callbacks
Use Cases
Function Parameter Changes
When function parameters change in a function pane, the parent step needs to know to update its list item preview:
# Function pane state is child of step state
# When function parameters change:
func_state.forward_to_parent_state('func')
# Step state's on_resolved_changed callbacks fire
# List item preview updates automatically
Nested Dataclass Updates
When a nested dataclass field changes, the parent may need to react:
# processing_config is a nested dataclass
processing_state.forward_to_parent_state('processing_config')
# Parent knows processing_config conceptually changed
Cross-Component Synchronization
Multiple widgets viewing the same hierarchical data stay synchronized:
# Parent widget shows summary
# Child widget shows details
# When details change, summary updates via parent notification
Architecture
forward_to_parent_state Method
The primary API for parent notification:
def forward_to_parent_state(self, field_path: Optional[str] = None) -> None:
"""Forward child state changes to parent state.
Notifies the parent state that a field has conceptually changed,
causing the parent's on_resolved_changed callbacks to fire.
Args:
field_path: Dotted path of field that changed. If None, auto-detects.
Raises:
RuntimeError: If state has no parent state.
"""
Key Behaviors
Reentrancy Guard:
# Prevents infinite loops during propagation
if getattr(self, '_forwarding_to_parent', False):
return
self._forwarding_to_parent = True
try:
# ... forward logic ...
finally:
self._forwarding_to_parent = False
Auto-Detection:
# If field_path not provided, detect from scope_id
# Example: scope_id "plate::step_0::function_1"
# Extracts: "function" (removes numeric suffix)
parts = self.scope_id.split('::')
last = parts[-1]
m = re.match(r'^(.+?)_\\d+$', last)
parent_field = m.group(1) if m else last
Callback Execution:
Fires all parent on_resolved_changed callbacks with the changed field.
Usage Examples
Basic Parent Notification
from objectstate import ObjectState
# Create parent and child states
parent_state = ObjectState(
scope_id="parent",
parameters=['field_a', 'field_b']
)
child_state = ObjectState(
scope_id="parent::child_0",
parameters=['detail_x', 'detail_y'],
parent_state=parent_state,
parent_field_name='child_field'
)
# Register callback on parent
def on_parent_changed(changed_paths):
print(f"Parent field changed: {changed_paths}")
parent_state.on_resolved_changed(on_parent_changed)
# In child, trigger parent notification
child_state.forward_to_parent_state('child_field')
# Output: "Parent field changed: {'child_field'}"
Function Pane Integration
Real-world example from function pane widgets:
# In FunctionPaneWidget.create_parameter_form()
func_state = ObjectState(
scope_id=f"{step_scope}::function_{index}",
parameters=['enabled', 'param1', 'param2'],
parent_state=step_state,
parent_field_name='func'
)
# Forward changes to parent for UI flash
def forward_to_step(changed_paths):
func_state.forward_to_parent_state('func')
func_state.on_resolved_changed(forward_to_step)
API Reference
- ObjectState.forward_to_parent_state(field_path=None)
Forward child state changes to parent state.
- Parameters:
field_path (str or None) – Dotted path of field that changed. If None, auto-detects from scope_id by extracting the last segment and removing numeric suffixes (e.g., “function_0” becomes “function”).
- Raises:
RuntimeError – If state has no parent state
Example:
# With explicit field path child_state.forward_to_parent_state('processing_config') # With auto-detection (scope_id="parent::child_0" -> field="child") child_state.forward_to_parent_state()
Best Practices
When to Use
Use forward_to_parent_state when:
Child state represents a field of parent (e.g., function in step)
Parent UI needs to update when child changes (e.g., list item preview)
Cross-component synchronization is needed
Hierarchical state changes should propagate upward
Field Path Selection
Explicit path: Use when field name differs from scope suffix
Auto-detect: Let system infer from scope_id for consistency
Dotted paths: Use for deep nesting (e.g., “config.subconfig.field”)
Performance Considerations
Forwarding is synchronous and lightweight
Reentrancy guard prevents exponential callback chains
Only fires parent callbacks, not grandparent (by design)
Use sparingly to avoid excessive parent updates