Class member access in Python Dash apps and callbacks

Viewed 634

I need help to understand some fundamental workings of a Python Dash app.

Since I need to implement a quite complicated application as a Dash app, I came up with a basic interface for my components.

class IComponent:
    def __init__(self, app:Dash, baseId:str) -> None:
        self._app = app
        self._id = baseId
        self._component = None

    def setup(self):
        """The calling scope will call this function to get the actual component to be added to the Dash layout."""
        self._create()
        self._registerCallbacks()
        return self._component

    def _create(self)
        """Implementation will create the Dash component and assign it to the self._component member."""
        raise NotImplementedError

    def _registerCallbacks(self):
        """Implementation will define all necessary callbacks for the component. Necessary component ids for interaction with other components may be passed to the implementation's constructor."""
        raise NotImplementedError

An implementation for a very simple component, e.g. a modal asking the user for a text input, would look like this:

class TextInputModal(IComponent):
    def __init__(self, app:Dash, baseId:str, o_storeText:str, t_openTrigger:str):
        super().__init__(app, baseId)
        self.o_storeText = o_storeText       # this is the id of a Store component to which the user text should be stored
        self.t_openTrigger = t_openTrigger   # this is the id to a button that triggers the modal

    def _create(self):
        header = dbc.ModalHeader("Provide Input")
        body = dbc.ModalBody(dbc.Input(id=self._id+"input"))
        footer = dbc.ModalFooter(dbc.Button(id=self._id+"confirm", children="Confirm"))
        modal = dbc.Modal(id=self._id, children=[header, body, footer], is_open=False)
        self._component = modal

    def _registerCallbacks(self):
        app = self._app
        id = self._id
        @app.callback(
            [Output(id, "is_open")],
            [Input(self.t_openTrigger, "n_clicks"),
             Input(id+"confirm", "n_clicks")],
            [State(id, "is_open"],
            prevent_initial_callback=True
        )
        def setVisibility(_, _, isOpen:bool):
            return not IsOpen

        @app.callback(
            [Output(self.o_storeText, "data")],
            [Input(id+"confirm", "n_clicks")],
            [State(id+"input", "value"],
            prevent_initial_callback=True
        )
        def setVisibility(text:str):
            return text

The calling scope would instantiate the TextInputModal, pass the necessary arguments textInputModel = TextInputModal(...), especially the component ids it depends on and then add it to a Dash layout (or another component's children) by calling the textInputModal.setup() method.

This way allows for better structure of my modules, code separation, avoiding code duplication etc. For further improvement of this approach I need to know:

  1. Can I use IComponent class members in my callbacks? (Other than I already successfully do to just assign ids and initialize callbacks!) I'm more thinking about e.g. putting some data into a class member, which is accessed by a callback to draw a plot upon a trigger. Or is this just a hidden violation of the "no globals" rule?

  2. How does the answer to (1.) change, if my Dash app is only used by ONE user locally (localhost)?

  3. How can I think of the very fundamental flow in a Dash app (assume I am a total beginner)? Where are Outputs/Inputs created (server/localhost, browser?)? What part of a callback is server-side, which one is client-side? Which data is shared between clients etc.? Where do callbacks "live" (think of my member variable question...) etc.

1 Answers
  1. That would be a violation of the "no globals" rule.

  2. It doesn't. You are still not guarantee that your app will work as intended. It might work. Sometimes. But I wouldn't rely on it.

  3. Callbacks are executed server-side (unless you use a client-side callback, then it is executed client-side). The server-side of Dash is stateless, so data is not shared between clients (unless you do it explicitly, e.g. via a shared cache). Hence you must keep state client side (e.g. in a Store element), or use a separate server-side state store (files on disk, a Redis cache, etc.).

Related