Can a paramterized class react to parameters outside of itself?

Complex applications can have deeply nested component hierarchies. Along the way, we may want to encapsulate state at different levels of the component hierarchy, say parent and child components. Further, the children of some containing component may want to react to changes in their parents state. Parameterized classes are the only way to share a components internal state with other components, but its not clear what the best practice is for sharing that state to children in a reactive way.

I’ve come up with the following, but it is ugly and won’t scale well:

import param
import panel as pn
pn.extension()

class StatefulComponent(param.Parameterized):
    name = param.String()

    def __init__(self, name, children, **params):
        super().__init__(**params)
        self.name = name 
        self.children = children

    # This is ugly, do I really have to do this?
    @param.depends('name')
    def update_subcomponent(self):
        self.children.parents_name = self.name

    def view(self):
        return pn.pane.Markdown(f"I am {self.name}")

class StatefulSubcomponent(param.Parameterized):
    name = param.String()
    # This is ugly too, I have to maintain a 
    # copy of the parent parameter to retain reactivity
    parents_name = param.String()

    def __init__(self, name, **params):
        super().__init__(**params)
        self.name = name 

    def view(self):
        return pn.pane.Markdown(
            f'I am {self.name}. My parent is {self.parents_name}')


sub = StatefulSubcomponent(name='Chapter')
main = StatefulComponent(name='Book', children=sub)
pn.Column(
    main.view, 
    sub.view, 
    main.update_subcomponent # This is also ugly
).servable() 

If you create a parameter that references the parent you can directly declare the dependency, e.g. here I’ve added a parent parameter, which I assign to. Now that we have made that declaration we can use param.depends to declare the dependency on the name parameter of the parent:

import param
import panel as pn
pn.extension()

class StatefulComponent(param.Parameterized):
    name = param.String()

    def __init__(self, name, child, **params):
        super().__init__(**params)
        self.name = name 
        self.child = child
        self.child.parent = self

    def view(self):
        return pn.pane.Markdown(f"I am {self.name}")

class StatefulSubcomponent(param.Parameterized):
    name = param.String()
    
    parent = param.Parameter()

    def __init__(self, name, **params):
        super().__init__(**params)
        self.name = name 

    @param.depends('parent.name', 'name')
    def view(self):
        return pn.pane.Markdown(
            f'I am {self.name}. My parent is {self.parent.name}')


sub = StatefulSubcomponent(name='Chapter')
main = StatefulComponent(name='Book', child=sub)
pn.Column(
    main.view, 
    sub.view, 
).servable()

The other thing you seem to have missed is that for callbacks with side-effects you do not pass the callback to Panel, instead you declare watch=True in param.depends, i.e. instead of:

    @param.depends('name')
    def update_subcomponent(self):
        self.children.parents_name = self.name
...
pn.Column(
    main.view, 
    sub.view, 
    main.update_subcomponent # This is also ugly
).servable() 

you would simply do:

    @param.depends('name', watch=True)
    def update_subcomponent(self):
        self.children.parents_name = self.name
1 Like

Aah, so elegant! Thank you. This has grounded my understanding in several different areas.

1 Like