How to let application state flow to component state?

In order to be able to maintain and further develop my apps I want to split them into seperate, reusable components.

I’m often faced with the question of how to do this and how to let shared application state flow down to the components (or up). These concepts of state and flows are very discussed and supported in front languages like React. But its not clear to me how this should be done with Panel.

So my question is really how to do this. To help the discussion lets take a concrete example with state flowing down into components.

The application below has a shared portfolio state and some components that have their own aggregation state. When the portfolio state changes this flow down to the components as an update to their portfolio state. But the aggregation state of the components is not changed.

import panel as pn
import param


pn.extension()

class RiskComponent(pn.viewable.Viewer):
    portfolio = param.String()
    aggregation = param.Selector(default="month", objects=["month", "year"])

    def __panel__(self):
        return pn.Column("## Risk Component", self.param.aggregation, self._get_risk, styles={"border": "1px solid pink"})
    
    @pn.depends("portfolio", "aggregation")
    def _get_risk(self):
        return f"{self.portfolio}, {self.aggregation}"

class Application(pn.viewable.Viewer):
    portfolio = param.Selector(default="power", objects=["power", "gas", "co2"])

    def _get_bound_risk(self):
        risk = RiskComponent(portfolio=self.portfolio)
        def _get_updated_component(component, **params):
            component.param.update(params)
            return component
        return pn.bind(_get_updated_component, component=risk, portfolio=self.param.portfolio)

    def __panel__(self):
        
        
        return pn.Column(
            "# Application",
            self.param.portfolio,
            pn.Row(self._get_bound_risk(),self._get_bound_risk(),),
            pn.Row(self._get_bound_risk(),self._get_bound_risk(),),
            styles={"border": "2px solid black"}
        )
        
Application().servable()

image

I like everything about the above python code except the function get_bound_risk. It is just boiler plate code. How can I replace _get_bound_risk function with one line of code?

Update

I can shorten via

def _get_bound_risk(self):
        risk = RiskComponent(portfolio=self.portfolio)
        pn.bind(risk.param.update, portfolio=self.param.portfolio, watch=True)
        return risk

But its still 3 lines of code instead of 1 :slight_smile:

The below is what I want. But it does not work

def _get_bound_risk(self):
    return RiskComponent(portfolio=self.param.portfolio)

I can do this with Panel components. Why can I not do this with Parameterized classes in general?

1 Like

Solution

I can do this now with param 2.0 using allow_refs.

This is HUGE. Thanks

import panel as pn
import param


pn.extension()

class RiskComponent(pn.viewable.Viewer):
    portfolio = param.String(allow_refs=True)
    aggregation = param.Selector(default="month", objects=["month", "year"])

    def __panel__(self):
        return pn.Column("## Risk Component", self.param.aggregation, self._get_risk, styles={"border": "1px solid pink"})
    
    @pn.depends("portfolio", "aggregation")
    def _get_risk(self):
        return f"{self.portfolio}, {self.aggregation}"

class Application(pn.viewable.Viewer):
    portfolio = param.Selector(default="power", objects=["power", "gas", "co2"])

    def __panel__(self):
        return pn.Column(
            "# Application",
            self.param.portfolio,
            pn.Row(RiskComponent(portfolio=self.param.portfolio),RiskComponent(portfolio=self.param.portfolio)),
            pn.Row(RiskComponent(portfolio=self.param.portfolio),RiskComponent(portfolio=self.param.portfolio)),
            styles={"border": "2px solid black"}
        )
        
Application().servable()
3 Likes

Awesome example! The borders make things so pretty :slight_smile:

allow_refs seems really powerful.

1 Like

Hey Marc - This is a cool solution. Does this also work with lazy loading. Such as in Tabs?

1 Like

I think so. But it would probably require an starting minimum, reproducible example to confirm.

Excerpt from Marc’s original post:

I’m often faced with the question of how to do this and how to let shared application state flow down to the components (or up). These concepts of state and flows are very discussed and supported in front languages like React. But its not clear to me how this should be done with Panel.

I seek to do the same thing. I would like to share parameters down, up, and across classes. To that end, I took Mark’s example and modified it to have a parent class and two sibling child classes.

I can share down, but I cannot currently share across or up.

The goal of my example is to share all 3 parameters across all 3 classes. The structure has a parent class “Flavor” with two child classes “Shape” and “Color”. I can share the flavor param of the Flavor class down to the Shape and Color classes successfully but Shape and Color params are not exchanged, and neither Shape nor Color is sharing its param up to the Flavor class.

image

import panel as pn
import uuid
#import plaidcloud.utilities.debug.wingdbstub
import param

class Color(pn.viewable.Viewer):
    flavor = param.String(allow_refs=True)
    shape = param.String(allow_refs=True)
    
    color = param.Selector(default="red", objects=["red", "blue"])
    
    def __panel__(self):
        return pn.Column("## Color", self.param.color, self._get_info, styles={"border": "4px solid red"})
    
    @pn.depends("flavor", "shape", "color")
    def _get_info(self):
        return f"Flavor:{self.flavor}, Shape:{self.shape}, Color:{self.color}"

class Shape(pn.viewable.Viewer):
    flavor = param.String(allow_refs=True)
    color = param.String(allow_refs=True)
    
    shape = param.Selector(default="square", objects=["square", "circle", "triangle"])

    def __panel__(self):
        return pn.Column("## Shape", self.param.shape, self._get_risk, styles={"border": "4px solid green"})
    
    @pn.depends("flavor", "shape", "color")
    def _get_risk(self):
        return f"Flavor:{self.flavor}, Shape:{self.shape}, Color:{self.color}"

class Application(pn.viewable.Viewer):
    app_id = str(uuid.uuid4())[:4]
    
    shape = param.String(allow_refs=True)
    color = param.String(allow_refs=True)
    
    flavor = param.Selector(default="beans", objects=['rice', 'beans', 'guac'])

    def __panel__(self):
        return pn.Column(
            '# Application {}'.format(self.app_id),
            self.param.flavor,
            pn.Row(self._get_my_stuff), 
            pn.Row(
                Shape(flavor=self.param.flavor),
                Color(flavor=self.param.flavor),
            ),
            styles={"border": "6px solid black"}
        )
    
    @pn.depends("flavor", "shape", "color")
    def _get_my_stuff(self):
        return f"Flavor:{self.flavor}, Shape:{self.shape}, Color:{self.color}"

if pn.state.served:
    Application().servable()

Very curious what I’ve missed here.

Sharing state across a Panel app is actually quite hard. The way I have solved this in the problem I’m working on is to have an App singleton which is shared across every component. It looks like this:


class State(param.Parameterized):
    # some example params
    trigger = param.Event()
    name = param.String()
    # other params go here

class App(param.Parameterized):
    state = State()

    def __init__(**params):
         super().__init__(**params)
         # do some stuff here to set things up
         # now put ourselves in the cache
         pn.state.cache['app'] = self

class GlobalStateMixin:
    @property
    def app(self):
        return pn.state.cache['app']


class Component(GlobalStateMixin):
    def __init__(**params):
        super().__init__(**params)

    # the below will trigger if somewhere else someone does self.app.state.name = 'foo'
    @param.depends('app.name', watch=True)
    def do_something(self):
        print(f'name has changed to {self.app.state.name}')

I’ve elided some imports here obviously but the basic idea is that you initialize the App singleton once during startup, then put it into the cache. By inheriting from the mixin, every component in your application now has access to that global state, and you can react on it with param just as you would any parameterized variable. You could even simplify this by just doing this for the State class above; I found it useful to wrap State inside App because I have some global functions I like to call, but you could just as easily separate the two. This can let you share reactive state without any regard for component hierarchy.

2 Likes

@jerry.vinokurov I will go ahead and out myself now as a n00b with too many things at once here (Panel, Params, React, Mixins). I’ve spent more time brute force trying to make the mixin work than I want to admit. Your mixin approach is very clever and makes total sense, but getting it hooked up to the point where the dots connect in a working example has been a bridge too far for me. I had a few attempts that I thought were going to work but I’ve come up empty so far. (Lack of skill, but not lack of effort :/)

Here are a couple of things that may or may not even be true, but I’ll share them here because they seemed to be the case to me at some point here in the last couple of days:

1.) I think maybe your @param.depends('app.name', watch=True) decorator should be @param.depends('app.state.name', watch=True)?

2.) Any chance your example name param is an unfortunate collision with a name namespace that Params makes use of underneath the hood? It seemed to me that I got further along when I renamed name to name2 or foo or something.

I’m not going to pollute this thread with a pile of my failed code, but I’ll share it here, if anybody wants to see examples of what does NOT work (mixin_6.py, mixin_7.py, mixin_8.py). GitHub - rea725/panel_stuff: Shared holoviz panel examples, just for fun and learning

@rea725

Yes, you’re right, I was a bit sloppy in my writing but it should be @param.depends('app.state.name', watch=True). It’s possible that there is a name collision as well, I hadn’t even thought of that when I put together my toy example.

I’ll take a look at your repo and see if I can offer any useful feedback there.

Not to beat this to death, but I reworked the example in the repo to create an MRE. Here’s what it looks like when I actually try to get things right instead of going off the top of my head:

import uuid
import panel as pn
import param


class State(param.Parameterized):
    # this is global state. it only has param objects, not widgets
    
    trigger = param.Event(allow_refs=True, nested_refs=True)
    foo = param.String()
    bar = param.String()
    thirty_seven = param.Integer(default=37)

    
class GlobalStateMixin:
    
    @property
    def app(self):
        return pn.state.cache['app']

    
class Thing1(GlobalStateMixin, pn.viewable.Viewer):

    # this is a class that has widgets locally, this is what people will see
    foo = pn.widgets.TextInput(name='Foo')
    bar = pn.widgets.TextInput(name='Bar')
    thirty_seven = pn.widgets.IntInput(name='thirty-seven', value=37)

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

    # the function below will listen for updates on any of the widgets
    # note that callbacks do not return any values. functions that have watch=True should
    # typically not return values
    @pn.depends('foo.value', 'bar.value', 'thirty_seven.value', watch=True)
    def widget_state_changed(self):
        self.app.state.foo = self.foo.value
        self.app.state.bar = self.bar.value
        self.app.state.thirty_seven = self.thirty_seven.value

    # we don't want to put any listeners on panel, as this function gets invoked
    # automatically in another context
    def __panel__(self):
        return pn.Column(
            '## Thing 1',
            self.foo,
            self.bar,
            self.thirty_seven,
            styles={'border': '4px solid blue'}
        )        

    
class Thing2(GlobalStateMixin, pn.viewable.Viewer):

    # these are just labels
    foo = pn.widgets.StaticText(name='Foo', value='')
    bar = pn.widgets.StaticText(name='Bar', value='')
    thirty_seven = pn.widgets.StaticText(value='', name='thirty seven')
    
    def __init__(self, **params):
        super().__init__(**params)
 
    def __panel__(self):
        return pn.Column(
            '## Thing 2',
            self.foo,
            self.bar,
            self.thirty_seven,
            styles={'border': '4px solid green'}
        )
    
    @param.depends('app.state.foo', 'app.state.bar', 'app.state.thirty_seven', watch=True)
    def update_labels(self):
        # we have to set the value property of the components
        self.foo.value = self.app.state.foo
        self.bar.value = self.app.state.bar
        self.thirty_seven.value = self.app.state.thirty_seven


class App(param.Parameterized):
    state = State()

    def __init__(self, **params):
        super().__init__(**params)
        # do some stuff here to set things up
        # now put ourselves in the cache
        pn.state.cache['app'] = self
        self.title = 'Two Components'
        self.thing1 = Thing1()
        self.thing2 = Thing2()
        
        self.template = pn.template.MaterialTemplate(
            title=self.title, 
            sidebar=[
                self.thing1
            ]
        )
        self.template.main.append(
            pn.Column(
                self.thing2,
            ),
        )

if __name__ == "__main__":
    App().template.show()


if pn.state.served:
    App().template.servable()

If you serve this, you will get two widgets, Thing1 and Thing2. If you change the values on the widgets of Thing1, the corresponding labels on Thing2 will update, even though at no point is there any explicit linkage between the two objects. All the communication is being done through app.state which is available as a property on any object that subclasses the GlobalStateMixin.

2 Likes

Very cool. Works as advertised. Thanks for making a go of this. I’m going to attempt to extend it a bit, so that there’s communication back the other direction, and maybe bi-bidirectionally with elements created directly in the parent App class.

Be aware that pn.state.cache is a global cache shared across sessions.

1 Like