Added component with dict id doesn't trigger related callback

Hi community,

the following callback should lacy load layout components. It works fine when the page first get loaded but the callback itself can return components that should trigger, but those don’t happen. Is it even possible?

thanks in advance

class LacyContainer(html.Div):
    class ids:
        container = lambda index: {
            "index": index,
            "type": "dash-router-lacy-component",
        }

    def __init__(self, children: Component, node_id: str, variables: Dict[str, any]):
        data_prop = {"data-path": json.dumps(variables)}

        super().__init__(
            children, disable_n_clicks=True, id=self.ids.container(node_id), **data_prop
        )

@self.app.callback(
      Output(LacyContainer.ids.container(MATCH), "children"),
      Input(LacyContainer.ids.container(MATCH), "id"),
      Input(LacyContainer.ids.container(MATCH), "data-path"),
      State(RootContainer.ids.location, "pathname"),
      State(RootContainer.ids.location, "search"),
      State(RootContainer.ids.state_store, "data"),
  )
  async def load_lacy_component(
      lacy_segment_id, variables, pathname, search, loading_state
  ):
      return LacyContainer.ids.container('possible-new-component')

Example rendering

For a bit more context, the loading dots are the components, that trigger the mentioned callback. The first two load as expected but only because the lacy components gets send on the url change. The last one under items gets rendered by the callback but doesnt trigger the callback itself. The component is in the layout with the right id etc.

Hello @Datenschubse,

When submitting a class component, its better to use @callback since its really outside the context of the app, try this:

from dash import callback, Input, Output, State

class LacyContainer(html.Div):
    class ids:
        container = lambda index: {
            "index": index,
            "type": "dash-router-lacy-component",
        }

    def __init__(self, children: Component, node_id: str, variables: Dict[str, any]):
        data_prop = {"data-path": json.dumps(variables)}

        super().__init__(
            children, disable_n_clicks=True, id=self.ids.container(node_id), **data_prop
        )

@callback(
      Output(LacyContainer.ids.container(MATCH), "children"),
      Input(LacyContainer.ids.container(MATCH), "id"),
      Input(LacyContainer.ids.container(MATCH), "data-path"),
      State(RootContainer.ids.location, "pathname"),
      State(RootContainer.ids.location, "search"),
      State(RootContainer.ids.state_store, "data"),
  )
  async def load_lacy_component(
      lacy_segment_id, variables, pathname, search, loading_state
  ):
      return LacyContainer.ids.container('possible-new-component')

Hi @jinnyzor, thanks for the response! Unfortunately this doesn’t solve the issue.
But would you say that it is generally possible to achieve this callback behaviour?

It should work, however, without a full view of what exactly you are doing. It’s hard to tell what is happening.

Could you please give an MRE for testing?

Sorry for the late response, had to make it Dash compatible. I hope the MRE is not too much effort to run, but the mentioned snippet is part of a bigger project I’m currently working on. The snippet can be found in this repo dash-router under src/dash_router/sync_router.py line 512:580.

The example is in the dash-router-example repo. Just install the pyproject.toml file and run dash_app.py.

Like I said, I hope it is not too much effort and tanks in advance!

A detailed explanation of the app example can be found in the README of the dash-router repo.

Hey @Datenschubse,

I wouldnt really consider that a MRE. XD

Anyways, I think you should bring the callback registration outside of the class component, as it uses MATCH, so it can be outside of there and wont possibly have conflicts by adding multiple times (no callbacks get triggered).

If that doesnt work, can you make a smaller example? :slight_smile:

Hey @jinnyzor, thanks for the response again.
Yeah totally get it, but was worth a try haha
Will write in this thread again when I have a MRE or solved it.

Will also post the router component soon, I think this can really enhance the development of large Dash applications especially with the 3.0 update! The lacy loading will just be a nice add on :smiley:

1 Like

Hey @jinnyzor

here is a legit MRE :smile:

# app.py
from dash import Dash, html, page_container, dcc, callback, MATCH, Input, Output
from pages.components import LacyContainer
import time

app = Dash(__name__, use_pages=True)

col_style = {
    "display": "flex",
    "flexDirection": "column",
    "gap": "2rem",
}

row_style = {
    "display": "flex", 
    "flexDirection": "row", 
    "gap": "4rem", 
    "margin": "2rem"
}

app.layout = html.Div(
    style=row_style,
    children=[
        html.Div(
            style=col_style,
            children=[
                dcc.Link("Page - 1", href="/page-1"),
                dcc.Link("Page - 2", href="/page-2"),
            ]
        ),
        html.Div(page_container),
    ],
)

@callback(
    Output(LacyContainer.ids.container(MATCH), "children"),
    Input(LacyContainer.ids.container(MATCH), "id")
)

def load_lacy(lacy_id):
    time.sleep(1.3)
    lacy_index = lacy_id.get('index')
    children = (
        html.Div(
            children=[
                html.Div(lacy_index),
                LacyContainer(dcc.Loading(display="show"), index=3),     
            ],
            style=col_style
        )
        if lacy_index < 4 else
        html.Div(
            lacy_index
        )
    )

    return children

if __name__ == "__main__":
    app.run(debug=True, port=3000)
# pages/page1.py
from dash import dcc, register_page
from .components import LacyContainer

register_page(__name__, path="/page-1")


def layout(**kwargs):
    return LacyContainer(dcc.Loading(display="show"), index=1)
# pages/page2.py
from dash import dcc, register_page
from .components import LacyContainer

register_page(__name__, path="/page-2")


def layout(**kwargs):
    return LacyContainer(dcc.Loading(display="show"), index=2)

# components.py
from dash import html


class LacyContainer(html.Div):
    class ids:
        container = lambda idx: {"index": idx, "type": "lacy-container"}

    def __init__(self, children, index, **kwargs):
        super().__init__(children, id=self.ids.container(index), **kwargs)

And the behaviour still persists. The seconds pattern matching component doesnt trigger its related callback.

Thanks in advance!

Is this what you are supposed to see?

I get similar on both pages. XD


This seems to be something keeping it from retriggering with it. I’m wondering if its not just that its within itself…

Not sure if you have seen this or not, but it seems to be a similar way you are trying to configure your app.

Thank you so much for taking the time !

I haven’t tried the intersection observer, but I think it doesn’t really meet my requirements, I can totally imagine that the lacy component is not in the viewport currently but should be rendered.

I was thinking about ditching the callback approach and go with SSE’s streaming the lacy components, but that would make the router not available for the normal Dash Flask setup :confused:

Would you recommend creating a GitHub issue? Would go for it if you say that the shown behaviour is in the scope of what should be possible with pattern matching callbacks.

And like I said many times, thanks again :raised_hands:

1 Like

Yeah that’s true, but I tried to mimic my initial setup as close as possible and the first lacy component wild be sent by the routing callback

I kinda think it should be available.

However, I’d test on dash 3 first to see if it wasnt fixed by happenstance.

I have already tried with dash 3.0.0rc3

1 Like

Okay, created the issue! :slight_smile: [BUG] Adding new Pattern Matching components don't trigger their related callback · Issue #3191 · plotly/dash · GitHub

1 Like