Dash is great, but I’m a bit disappointed by the complexity and pitfalls involved when trying to use python to create composed/abstracted components.
As soon as one try to encapsulate some behavior using callbacks (within the fully-stateless and multi-server compatible realm) things becomes complex.
I know that callback pattern / AIO components are a solution which usually work, but it is kinda bulky. Another problem is that the app object need to be “distributed” to where the callback is used. (unless the server only host a single app and it’s possible to use the global callback list through dash.callback)
Have anyone successfully built and used a decent python component library? Would you recommend going down that path? Or is it better to just use react for more complex components?
I believe some components/structures can neatly be formulated in Python, while others a better suited for React. Could you provide example(s) of what you are trying to achieve?
One example: A generic multiplexer (basically Tabs, but where the UI for selecting the tab is decoupled from the implementation). In my use-case the UI is a dropdown.
This I’m not sure how to do using AIO/pattern callbacks since I the number of outputs are a function of the particular layout (not dynamic though)
import typing as ty
import dash
from dash import html, dcc, Dash, Output, Input, State
from dash.development.base_component import Component
# Brittle for multi-server environments
def unique_id(cell=[0]):
cell[0] += 1
return "_gen_id_" + str(cell[0])
def Multiplexer(panes: ty.Dict[str, Component], selected: ty.Union[Input, State]):
"""
Displays the pane with key corresponding to the ``selected`` input.
:param panes: Dict of dash component to multiplex based on the key
:param selected: The input or state holding the key which should be displayed
:return:
"""
pane_ids = {
k: unique_id()
for k in panes
}
style_hidden = {"display": "none"}
style_displayed = {}
@dash.callback(
[
Output(pane_id, "style")
for pane_id in pane_ids.values()
],
selected,
)
def cb(selected_key):
return [
style_hidden if selected_key != k else style_displayed
for k, p in panes.items()
]
return html.Div([
html.Div(p, id=pane_id, style=style_hidden) for p, pane_id in zip(panes.values(), pane_ids.values())
])
if __name__ == '__main__':
app = Dash(__name__)
app.layout = html.Div([
dcc.Dropdown(
id="dropdown",
value="A",
options=[
{"label": "A", "value": "A"},
{"label": "B", "value": "B"},
]),
html.Hr(),
Multiplexer({
"A": html.Div("A"),
"B": html.Div("B"),
}, Input("dropdown", "value")),
])
app.run_server()
And as I mentioned - packaging the generic components into modules is awkward since the app instance need to be available when defining the callbacks for the generic components [1]. (We host multiple apps using the same dash server)
The alternatives I’ve considered:
Requiring the component user to pass in the app to the component
Using a “object” module which the can bind an to anapp. Ie. creating a set of component classes per app.
Using importlib hackery to do something similar to (2)
[1] Maybe there’s better ways of hosting multiple apps which allow the dash.callback method to work?
All types of components where you basically want to intercept/wrap callbacks to provide a nicer interface for the programmer.
An example would be a DatePicker where the callback receive a date object instead of ISO formatted string.
Or if i want to accept/display a timedelta as hours, but want to process the values as milliseconds. Not needing to make sure the callback processing is in sync with the particular way of presenting/accepting the input.
For the intercept/wrap callbacks stuff I have created the DashProxy abstraction in dash-extensions. It defines the concept of transformations which is basically transformation/wrapping of callback to achieve a nicer syntax. As a simple example, the NoOutputTransform adds a dummy output to callbacks that have no output,
class NoOutputTransform(DashTransform):
def __init__(self):
super().__init__()
self.components = []
def transform_layout(self, layout):
children = _as_list(layout.children) + self.components
layout.children = children
def _apply(self, callbacks):
for callback in callbacks:
if len(callback[Output]) == 0:
output_id = _get_output_id(callback)
hidden_div = html.Div(id=output_id, style={"display": "none"})
callback[Output] = [Output(output_id, "children")]
self.components.append(hidden_div)
return callbacks
def apply_serverside(self, callbacks):
return self._apply(callbacks)
def apply_clientside(self, callbacks):
return self._apply(callbacks)
thus allowing syntax like,
@app.callback(Input("btn", "n_clicks")) # no Output is OK
def func(n_clicks):
print(f"Click count = {n_clicks}")
I believe it should be relatively simple to create a transform for the date picker case.
For the tab stuff, I think I would just use a few functions. Alternatively your could use a setup where you create the panes as DashProxy objects and mount them on the main app, possibly using a PrefixIdTransform to avoid id collisions.
For issue of packing components into modules, you could also use the DashProxy object to collect callbacks, or you could use the new @callback syntax (i.e. without the app). I was unsure from your last post if you have experienced issues using this syntax? If you do, you could try out dash-extensions; I have implemented the logic differently here as compared to the main Dash library.
Thanks, that library looks interesting. I will check it out. (Although… the fact that you had to create a proxy object like that kinda reinforce my feeling that dash is missing some parts when it comes to serverside composition and abstraction)
I’m aware of the @callback syntax, but we host multiple apps using the same server which cause problems. Not sure I understand how using the DashProxy would help. If I understand correctly that’s just a wrapper class around Dash. So I would still need to distribute the dash app proxy object (which is app specific) to the modules.
But I guess it would be possible to create my own global/non-app specific callback wrapper which collects the callbacks and register them on the correct app object after the app has been created