Ways to compartmentalize Dash apps

#1

Hi guys,

As probably most of you guys know, Dash app scripts, especially with callbacks involved can get a bit messy. I wanted to compartmentalize things into modules, but it still is a bit hard to get right.

My current solution looks somewhat like this:

Where the key lies in the element class:

"""Plotly Dash app layout module."""
import dash_html_components as html
from dash import Dash


class Element:
    """Plotly Dash app element to standardize adding HTML divs and callbacks.

    Arguments:
        layout: HTML Div describing this element.
    """

    def __init__(self, layout: html.Div):
        self.layout = layout

    def register(self, app: Dash):
        """Register callbacks in app.

        Arguments:
            app: App to register callbacks in.

        Don't forget to add register calls of child elements!
        """
        raise NotImplementedError

and a example layout module looks like:

"""Main layout module."""

import dash_html_components as html
from dash import Dash
from dash.dependencies import Input, Output

from dash_project_layout.layout import Element

# Create layout here.
layout = html.Div(
    [
        html.Button("Click me", id="clickme-button"),
        html.Div("Waiting for a click...", id="content-div"),
    ],
    id="main-container",
    className="main-container",
)


# Add callbacks in derived class.
class MainElement(Element):
    def register(self, app: Dash):
        @app.callback(
            Output("content-div", "children"), [Input("clickme-button", "n_clicks")]
        )
        def content_div_children(n_clicks):
            if n_clicks < 10:
                return "{} clicks".format(n_clicks)
            return "Hello world!"


main = MainElement(layout)

This enables you to include the layout of different elements quite easily in other modules, and also gives you the opportunities to include one element’s .register(app) method in the parent element’s register call.

Just for completeness, the running script looks like:

import dash
from dash_project_layout.layout.main import main

app = dash.Dash(__name__)
app.layout = main.layout
main.register(app)
run = app.run_server


def cli():
    """Dash server command line interface (CLI)."""
    import argparse

    parser = argparse.ArgumentParser(
        prog="Dash Project Layout", description="Example Dash Project Layout."
    )
    parser.add_argument("-d", "--debug", action="store_true", help="turn on debug mode")
    args = parser.parse_args()

    run(debug=args.debug)


if __name__ == "__main__":
    cli()

What are your thoughts or how do you tackle the complexity of Dash apps?

1 Like
#2

For multipage apps, I typically structure my projects in a similar way to the first project layout described in the section Structuring a Multi-Page App of the Dash Docs. This means just having a simple layout attribute in each page module containing that pages layout, and having the router register each of them.

You can see this in my Slapdash project.

The two main drawbacks of this approach, which I consider to be friction points for development of more complex Dash apps are:

  • No builtin name-spacing of pages, so you have to make sure you don’t duplicate component IDs across your pages, which become a bit of a headache very quickly. See this issue and this community discussion.
  • If you use the workaround described in the URLs page of the Dash docs to enable callback validation for multi-page apps, you won’t be able to organise your code into different page modules containing callbacks and layouts for each page due to Dash’s eager callback validation, as described in this issue.
#3

Thanks for sharing!

The main difference I see is that your setup always assumes the same “app” is used and layout elements’ callbacks are in that sense more or less tied to that app.

The multipage-ness is cool though! Never quite used that, yet.

#4

I’m not sure I agree with that suggested limitation. Provided that you take care to namespace your component IDs, I don’t actually think there’s anything stopping you from organising your code into a library of different pages that contain separate pairings of layout+callbacks. You could then have different apps that import from that package of sub-apps as needed. The layouts for the router can come from arbitrary sources, so could easily be external modules.

Or did you mean something different to that?

#5

If I understand correctly, you import the Dash app instance from ..app import app in every layout Python module directly, binding your callbacks right in the layout module, too. This would make using the same layout components (and callbacks) in a completely different app more difficult (say external to the package), right?

In any case, since the callback Input/Output component identifiers can come from arbitrary layout elements, true portability will be hard to deliver in most cases. A def get_layout() function can help solve that with some arguments though. But I don’t know if that’s the most readable solution we can come up with.

#6

Ah yes, that’s true. There’s ways around that; although I haven’t yet come up with a super elegant solution, so haven’t included it in Slapdash. At the end of the day you do need to make some kind of assumption about where the Dash instance is going to come. The approach in those pages in Slapdash isn’t very portable for sure.

Flask has a mechanism that helps with these import issues: its application context. So you push the Flask instance of your app as an application context, then flask.current_app becomes a proxy for that instance that you can import anywhere within that application context, without making assumptions about where the instance was defined. I actually do this in Slapdash here.

Dash doesn’t have its own version of this proxying, so one clunky way to solve the problem could be to attach the Dash instance to the Flask instance (eg in Flask.config) then you’re only assuming that your various parent apps have pushed an application context with a Dash instance attached, nothing about where the instance was defined.

It would be great if Dash had its own equivalent of a current_app context.

But yeah, without namespacing of these pages, they’re still not that portable I agree.