Ideas for namespacing Component IDs?

One challenge I’ve hit upon when producing multi-page apps is ensuring that component IDs for each page are unique., which is a requirement for Dash apps. This starts to become challenging with even a handful of pages, as callbacks across pages can do very similar things, resulting in the desire to use similar IDs. This also makes re-using/copying pages for new (and similar pages) difficult, as you have to go through and uniquify the IDs.

My current best approach to namespacing layout fragments is to traverse the layout tree, patching each component’s ID (if it has one) with the desired prefix.

Callbacks are a bit uglier though as I currently use f-strings templates in order to to namespace them with a parameter. eg:

ID_PREFIX = "page1_"

@app.callback(
    Output(f"{ID_PREFIX}some_div", "children"), 
    [
        Input(f"{ID_PREFIX}text-input", "value"), 
        Input(f"{ID_PREFIX}num-input", "value")
    ]
)
def some_func(text, num):
    return "output"

Curious to know if anyone has come up with some cleaner strategies than this.

I do the exact same thing - preface IDs with a standard prefix unique to each page. I’ve tried and failed to come up with a better way to do this.

I’ve tried a number of different approaches, I’m not sure any of them are good ideas but I have the same basic principle which seems to be working for me:

  • Have all IDs be class variables and swap them out dynamically

I’m not doing this just for pages, I’m doing this for user defined widgets in multiple pages, unfortunately I can’t share that code though.

Here is a very rough non-complete approach to how I think I would tackle this problem for multi-pages, for each Page have a class that looks like this:

from page import DashPage, ID, page_callback

class MyPage(DashPage):
    button: ID = 'button'
    output: ID = 'output'

    def get_layout(self):
        return html.Div([html.Div(id=self.output),
                         html.Button('Hello', id=self.button)])

    @page_callback(
        output=dash.dependencies.Output(output, 'children'),
        inputs=[dash.dependencies.Input(button, 'n_clicks')]
    )
    def click_count(self, n_clicks):
        if n_clicks is None:
            return dash.exceptions.PreventUpdate
        return n_clicks

Then in the page module have a metaclasses and callback class that register everything that is the MyPage class does and dynamically swap out the IDs for namespaced values:

# ID Type
class ID(str):
    pass


# Helper Classes
class DashPageMeta(abc.ABCMeta):
    def __new__(mcs, name, bases, dct):
        new_class = abc.ABCMeta.__new__(mcs, name, bases, dct)
        types = typing.get_type_hints(new_class)
        for item_name, item_type in types.items():
            if issubclass(item_type, ID):
                item_value = getattr(new_class, item_name)
                attribute_value = f'page-{dct["__qualname__"]}-{item_value}'
                setattr(new_class, item_name, attribute_value)

        if name != 'DashPage':
            PAGE_CLASSES[name] = new_class
        return new_class


class DashPage(metaclass=DashPageMeta):
    @abc.abstractmethod
    def get_layout(self, *args, **kwargs):
        pass


class PageCallbackDecorator:
    output = None
    inputs = None
    state = None

    def __init__(self, func):
        self.func = func

    def __set_name__(self, owner, name):
        if not hasattr(owner, 'callback_funcs'):
            owner.callback_funcs = []

        owner.callback_funcs.append((name, self.output, self.inputs, self.state))

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)


def page_callback(output=None, inputs=None, state=None):
    PageCallbackDecorator.output = output
    PageCallbackDecorator.inputs = inputs
    PageCallbackDecorator.state = state
    return PageCallbackDecorator

And then have 2 functions, one that builds the layout and one that adds the callbacks. My approach is sufficiently different I’m just going to have to provide a general idea of how you would do this:

def build_layout():
    layout = []
    for page_name, page in PAGE_CLASSES.items():
        # Here I pass in the user to page, e.g. page(user)
        # allowing me to return custom pages
        page_layout = page().get_layout()

        # Here you might want to wrap the layout with standard page controls or themes

        layout.append(page_layout)


def add_all_callbacks(app):
    for page_name, page in PAGE_CLASSES.items():
        if not hasattr(page, 'callback_funcs'):
            continue

        page_instance = page()
        for method_name, output, inputs, state in page_instance.callback_funcs:
            # You actually need to test if output is an iterable here
            original_value = output.component_id
            output.component_id = f'page-{page.__dict__["__qualname__"]}-{original_value}'
            # Iterate through inputs and states and do the same
            # Then add to the real app callback
            callback_method = getattr(page_instance, method_name)
            app.callback(output=output, inputs=inputs, state=state)(callback_method)

You can probably simplify this a lot, as I say my name-spacing is actually fairly more complicated and dynamic, e.g. I’m passing user configuration in to each page class so each page can be custom.

But hopefully this gives you some ideas.

1 Like

Interesting approach @Damian, creating at abstraction that allows you to delay registration of callbacks with Dash until you can marshal them all together with an ID prefix.

@sjtrny has also put together an abstraction for working with multi-page Dash apps that also follows the general pattern of delaying callback registration in enable ID prefixing.

1 Like

I think that name-spacing components should ideally be supported directly by the Dash API, and hopefully this discussion has added support for adding this enhancement.

I dug around and found an old issue that was originally inspired the the problem of not being able to use Dash app examples more than once in the dash-docs app, due to ID duplication. I really like @alexcjohnson’s proposal of having a Section class that has the same interface as the Dash class, but is registered against a parent Dash app with namespace param:

app1.py

from dash import Section
section1 = Section()

section1.layout = html.Div(...)

@section1.callback(...)
def ...

app.py

from app1 import section1

app = Dash()
app.section(section1, scope='section1')
4 Likes

I’ve added support for nesting sections and pages https://github.com/sjtrny/dash-multipage

The interface/syntax is a little rough around the edges but functionally it follows a similar pattern to @alexcjohnson’s suggestion. It could be massaged to more closely match the suggestion.

The goal here is to get this to a complete state so that we can submit a PR for dash. Any help is welcome on my repo!

The excerpt below is from index.py demonstrating a more complex app.

app = MultiPageApp(__name__, external_stylesheets=external_stylesheets)

server = app.server

class IndexPage(Page):
    def __init__(self):
        self.layout = header

class IndexSection(Section):
    def __init__(self):
        self.routes = [
            Route('/', IndexPage),
            Route('/page1', Page1),
            Route('/page2', Page2),
            Route('/page3', Section3),
        ]

app.root(IndexSection)

if __name__ == '__main__':
    app.run_server(debug=True)
1 Like

Progress continues! (https://github.com/sjtrny/dash-multipage)

I’ve managed to further simplify the interface. Now it is almost entirely compatible with the @alexcjohnson style.

Create a multipage app

app = MultiPageApp(__name__)

# Configure routing
routes = [
    Route('', IndexPage()),             
    Route('/page1', Page1()),           
    Route('/page2', section2_routes),   # Section containing pages and further sections
]

app.set_routes(routes)

if __name__ == '__main__':
    app.run_server(debug=True)

Create a single page app (@alexcjohnson style):

app = MultiPageApp(__name__)

page = Page()
page.layout = html.Div(header.children + [
        html.H3('Home'),
        dcc.Interval(id='interval-component', interval=1*1000, n_intervals=0),
        html.Div(id='display-value'),
    ])

@page.callback(Output('display-value', 'children'), [Input('interval-component', 'n_intervals')])
def my_callback(n_intervals):
    return f"Seconds since load: {n_intervals}."

# This will serve the page at "/"
app.set_routes(page)

if __name__ == '__main__':
    app.run_server(debug=True)
1 Like

I’ve come to the conclusion that with the current architecture of Dash there are too many drawbacks with the aforementioned approach.

Namely:

  1. namespacing must be done dynamically (to avoid potential namespace conflicts)
  2. use of Location components is handicapped

For 1. the effect is that there is a drastic increase in page load and update time. For 2. we can only have a single Location component, which forces apps to be tightly coupled to the interface I created.

To resolve both of these issues, I have moved towards an approach that I first saw outlined by @Vlad Multiple Dashboards?. I think for the time being, this is a more practical approach until Dash has native support for multi-page apps.

Creating a single page app now looks like

server = Flask(__name__)

app = Page1(name="home", server=server, url_base_pathname="/")

if __name__ == "__main__":
    server.run(host="0.0.0.0")

Creating a multi-page app (Method 1)

server = Flask(__name__)

index_app = IndexApp(name="home", server=server, url_base_pathname="/")
section_app = Section(name="section", server=server, url_base_pathname="/app1")

if __name__ == "__main__":
    server.run(host="0.0.0.0")

Creating a multi-page app (Method 2)

class MyApp(MultiPageApp):
    def get_routes(self):

        return [
            Route(IndexApp, "index", "/"),
            Route(Section, "home", "/app1")
        ]

server = Flask(__name__)

app = MyApp(name="", server=server, url_base_pathname="")

Code and install details: https://github.com/sjtrny/dash-multi

1 Like

I’m wondering if @chriddyp or @alexcjohnson have any thoughts about strategies for namespacing Dash apps? There’s clearly a number of people interested in helping with this feature, but I feel like we’re bumping up against the limits of what we can contribute without some input from the Dash core dev team.

@nedned let’s continue this discussion at https://github.com/plotly/dash/issues/637 - I added some more thoughts there, but it’s still not a completely fleshed-out proposal.