Motivated by the conventions suggested for AIO-s, but thinking we still have a couple of weaknesses in that approach. I looked into how to make a few improvements that we felt important for our work.
We wanted, in particular, an approach allowing:
Nesting or “compos-ability” - being able to nest components within components, several levels down
Address-ability - within callbacks, being able to access or refer to the properties of components within components, several levels down (in practice, most likely only the immediate or second level down will be necessary).
Scalability - from the coding perspective at least, achieve a coding style allowing brevity and clarity. Best if execution performance is not severely degraded.
I came up with a variation on AIO-s. For the moment I’m calling them “Compound Components”, if only for the purpose of distinguishing them from AIO-s in any discussion.
The variation is minimal, mainly consisting of using concatenated strings for the component IDs.
The thing is that with that simple change I believe we achieve all the above additional advantages. Plus I believe we keep all the nice properties of AIO-s, like allowing the “packaging”, publication and sharing of components in the community, with a consistent interface.
You can check out a full, yet brief example here.
I am clear we get nesting and address-ability and believe scalability as well. I believe this approach should not reduce callback performance when compared to achieving the same layout without nested components. But I don’t know enough about performance related to callback string IDs. I will appreciate comments from the community in regards to this aspect.
At our team, we’re not planning to scale this up to many dozens of components or nesting them too deep, so it seems this should work for us fairly well. I hope others will also find this approach useful.
Very nice!! Cool to see folks taking this a step further.
One advantage of the dict IDs is that they allow for the callbacks to be defined outside of the __init__ function, i.e. when the component is imported rather than when it is used. This allows the callbacks to be defined even if the component is first defined within a callback instead of within the initial layout (for example, within a multi-page app).
One major obstacle with this approach is the inability to add/remove AIO components at runtime.
Since callbacks are registered per instance, only the instances added before the Dash server starts will have their callbacks registered.
@chriddyp are there any plans to allow registering callbacks after the Dash server started?
In theory it would not be too hard to implement this if the new callbacks are not active before the next iteration of the event loop (and that’s a reasonable limitation to have).
IMHO, eliminating this no-registering-callbacks-after-dash-server-starts limitation and the one-callback-per-output rule would greatly improve development experience for complex dashboards/apps. (Both can be circumvented right now through various hacky solutions, but a robust library solution would be much better)
I believe the callbacks are actually registered when the AIO component is imported, not instantiated. Try adding a print statement just before the callback registration to verify. This is because they are within the class but not in the init.
The challenge with this is that dash apps can be scaled across multiple processes scaled across different servers; if one callback created new callbacks then those new callbacks might only be registered in one of many processes/servers that is serving the dash app. This is the “stateless” nature of Dash (and common across classic web server architectures) that enables it to scale to hundreds or thousands of concurrent users.
The pattern matching callbacks is the solution here - it defines the pattern of “potential” components that can exist on the page at future times and these patterns are defined upfront when the app starts by every process and server serving the dash app.
I see, that’s a good point. For my use a sufficient improvement would be to allow just new clientside callbacks (so the servers falling out of sync would not matter), but that’s probably not helpful enough for the majority of Dash users. Still, it would be great if AIO components had some more flexibility.
For me one awkward thing with the current pattern-matching callback implementation (also mentioned here: Can you use AIO components in an AIO component?) is using an AIO inside another AIO. A way to accomplish this could be to have the aio_id of the inner AIO be the outer AIO’s full ID, and being able to MATCH on values inside these nested dictionaries. However nested dictionaries as component ids are not supported at the moment.
One doable alternative is to have the inner AIO use the same aio_id as the outer AIO’s aio_id, but that pollutes the namespace. Another alternative is to store the ID of the inner component in a Store inside the outer component, and then manually check in a callback if the ID of the subcomponent is the right one. This approach I think has no theoretical limitations, but it’s a bit awkward to use.
Yeah that makes sense, and would certainly be doable. A long time ago I wrote a prototype of clientside callbacks that could be embedded within the layout, check out the examples in Dash Clientside Transformations by chriddyp · Pull Request #142 · plotly/dash-renderer · GitHub. It was a really cool idea but we decided to scrap it because we felt that learning it was “too different” than regular callbacks. We wanted an easier interface to switch from regular callbacks to clientside callbacks. But maybe it’s time to bring back that old PR back…
Yeah I could see how this could get messy. Do you have a simple example use case of this? It might be easier to work through some alternative API design ideas with a simple code example.
Yes, dynamically inserting (clientside) callbacks in layout sounds great. It would be great if the actual code could be a parameter of that layout code component, but even just putting in the name of an externally defined function would be great.
I can write a small example later this week (I’m in a bit of a hurry right now, I don’t have a self-contained example at hand). The particular example I had today was this situation:
an AIO component for a toggle (button group with persistent selection),
an AIO component for a collapsible panel, whose header contains a toggle (an instance of the first AIO component) which determines if the panel is collapsed or not.
Both AIO components could be written as React components, but for some reasons unrelated to Dash that was not done. Anyway, the way this is implemented right now is that the panel components creates a child toggle with the same aio_id. The drawback (as we discussed above) is that the user of this panel component might have their own toggle and give it the same aio_id, without realising that its already in use. In some other cases I resolved similar issues by using a Store (which stores child ids), it’s not too bad but requires putting in some more thought into the design.
I can write a code example (probably a very simplified version of the situation above) in a few days…
It actually shows two related situations, one that would be solved with nested IDs, and another that would be solved with on-the-fly client-side callbacks.
Currently both can be solved with a not very elegant use of stores.