Hey everyone –
I’ve been spending some time over the last few weeks working on a pattern for modularizing components and callbacks into a single, reusable import.
Here is a proposal with some yet-to-be-published APIs and examples. Would love any feedback that you might have. Thanks for reading!
Example 1
Let’s consider a simple reusable component: A markdown block that contains a color picker:
chriddyp_components.py
from dash.g import app # < This is new! More on this below
class MarkdownWithColor:
_component = f'{__module__}.{__qualname__}' # Evaluates to chriddyp_components.MarkdownWithColor
class ids:
dropdown = lambda instance: {
'component': _component,
'subcomponent': 'dropdown',
'instance': instance
}
markdown = lambda instance: {
'component': _component,
'subcomponent': 'markdown',
'instance': instance
}
@app.callback(
Output(ids.markdown(MATCH), 'style'),
Input(ids.dropdown(MATCH), 'value'),
State(ids.markdown(MATCH), 'style'),
)
def update_markdown_style(color, existing_style):
existing_style['color'] = color
return existing_style
def __init__(self, text, colors=['black', 'red', 'blue', 'green'], markdown_options=None, dropdown_options=None, instance=None):
if instance is None:
instance = str(uuid.uuid4())
return html.Div([
dcc.Dropdown(
id=ids.dropdown(instance),
colors=[{'label': i, 'value': i} for i in colors],
value=colors[0],
**(dropdown_options if dropdown_options is not None else {})
),
dcc.Markdown(
id=ids.markdown(instance),
style={'color': colors[0]},
**(markdown_options if markdown_options is not None else {})
)
])
app.py
from chriddyp_components import MarkdownWithColor
app = dash.Dash(__name__)
app.layout = html.Div([
MarkdownWithColor('Example 1'),
MarkdownWithColor('Example 2'),
])
Let’s break this down:
-
from dash.g import app
- This is proposed and not yet published to Dash. Here’s the PR. This import allows us to define anapp.callback
without needing to importapp
fromapp.py
-
class ids
- This is just a convention that I came up with. It keeps the code DRY and allows consumers to access the unique IDs of this component. More on this in the next example. -
@app.callback
- Callbacks need be defined in advance. By defining@app.callback
within theclass
but outside an__init__
method, this@app.callback
will be called when the component is imported (from chriddyp_components import MarkdownWithColor
) -
class MarkdownWithColor
- We’re using a class just to keep things modular. We’re not using any object oriented features here. Thisclass
allows us to define things likeMarkdownWithColor.ids.dropdown
without polluting the file’s namespace. This allows you to write multiple components within the same file. -
**(dropdown_options if dropdown_options is not None else {})
- This line passes allows component consumers to override dropdown options as they see fit. This inlineif
expression just keeps things on a single line and is just my own convention / preference for keeping code terse. - Pattern Matching IDs - The ID would get evaluated as:
The keys ({ 'component': 'chriddyp_components.MarkdownWithColor', 'subcomponent': 'dropdown', 'instance': <some-long-UUID-expression> }
component
,subcomponent
, andinstance
) is a convention that I came up with. The important thing with these IDs is to avoid ID collision - These IDs need to be unique within the app. As more community members publish components, it’s important that we keep these IDs unique across the community.-
component
combines the modular name and the component name, so this is pretty likely to be unique. -
instance
is like the “id” of this particular component. The callback functionality is bound to every instance of the component via the pattern matching callback that usesMATCH
. An alternative name here would beid
, but then we haveid={'id': ...}
which feels a little redundant. Open to suggestions here! -
subcomponent
refers to the… subcomponent within this component. This allows consumers to bind bespoke callback interactivity to particular pieces of the component. This is discoverable via theids
class.
-
This pattern allows consumers to bind their own callbacks to subcomponents. Here is an example where the color dropdown is bound to a graph:
```python
from chriddyp_components import MarkdownWithColor
app = dash.Dash(__name__)
app.layout = html.Div([
MarkdownWithColor('Example 1', instance='colorpicker-1'),
dcc.Graph(id='my-graph')
])
@app.callback(
Output('my-graph', 'figure'),
Input(MarkdownWithColor.ids.dropdown('colorpicker-1'), 'value')
)
def update_graph(color):
return px.scatter(...).update_traces(marker_color=color)
MarkdownWithColor.ids.dropdown('colorpicker-1')
is simply a new convention. It is a function that returns an ID dictionary. So, the expression above is equivalent to:
@app.callback(
Output('my-graph', 'figure'),
Input({'component': 'MarkdownWithColor', 'subcomponent': 'dropdown', 'instance': 'colorpicker-1'}), 'value')
)
Example 2 - Configurable Callbacks
Now consider a component whose interactivity (via callbacks) is configurable. These parameters can be passed into the callback via a dcc.Store
. There isn’t anything new here required to do this.
This example adds a new property called font_size
that is passed into a MATCH
'd dcc.Store
and into the callback via State
:
from dash.g import app # < This is new! More on this below
class MarkdownWithColor:
_component = f'{__module__}.{__qualname__} # Evaluates to chriddyp_components.MarkdownWithColor
class ids:
dropdown = lambda instance: {
'component': _component,
'subcomponent': 'dropdown',
'instance': instance
}
markdown = lambda instance: {
'component': _component,
'subcomponent': 'markdown',
'instance': instance
}
store = lambda instance: {
'component': _component,
'subcomponent': 'store',
'instance': instance
}
@app.callback(
Output(ids.markdown(MATCH), 'style'),
Input(ids.dropdown(MATCH), 'value'),
State(ids.markdown(MATCH), 'style'),
State(ids.store(MATCH), 'data'),
)
def update_markdown_style(color, existing_style, component_params):
existing_style['color'] = color
existing_style['fontSize'] = component_params['font_size']
return existing_style
def __init__(self, text, font_size=12, colors=['black', 'red', 'blue', 'green'], markdown_options=None, dropdown_options=None, instance=None):
if instance is None:
instance = str(uuid.uuid4())
return html.Div([
dcc.Store(id=ids.store(instance), data={'font_size': font_size}),
dcc.Dropdown(
id=ids.dropdown(instance),
colors=[{'label': i, 'value': i} for i in colors],
value=colors[0],
**(dropdown_options if dropdown_options is not None else {})
),
dcc.Markdown(
id=ids.markdown(instance),
style={'color': colors[0]},
**(markdown_options if markdown_options is not None else {})
)
])
Example 3 - Sharing Intermediate Results
In some cases, the callback may perform expensive intermediate computations that consumers may want to access.
For example, consider an EnhancedDataTable
that performs the filtering operations in the callback. A consumer may want to access the intermediate filtered data in their own callback that e.g. updates their own graph.
This intermediate data could be published to an embedded dcc.Store
like the example above. However, in some cases the intermediate data is too large to publish to the client.
Essentially we’re running into the classic “Sharing Data Between Callbacks” problem.
One solution is for component authors to save this data to Redis or the file system and provide a unique session ID as part of the API.
Another solution we’re exploring is adopting an API similar to @emil’s ServersideOutput
- Could the component author write this data to a unique ID via ServersideOutput
and could we allow consumers to access that data in their own callback. There are some nuances to work out here, but this is a solution that we’ll explore.
Examples of Modular Components
Modular components can come into play when:
-
You have an interactive component + callback that you use multiple times across your app or project
-
Your component’s interactivity can be written as a callback in Python code (rather than written in JavaScript and encapsulated in React)
-
Enahnced DataTable component that does the filtering, sorting, and paging in Pandas (or Vaex or SQL!) automatically. Consider the examples in the docs. Currently community members are writing the same-ish 10s-100s lines of filtering Pandas code in every project across every DataTable
-
Graph + Data - Imagine an “Our World In Data”-like component that displays a standard graph with a tab to view the underlying data.
-
Datashader component that combines the boilerplate holoviews code into a single
DataShade()
component
Community Components
I’m really excited about this idea because it lowers the barrier of entry to creating & sharing custom components and layouts. I am looking forward to community members sharing and publishing reusable, generic interactive components that hook into powerful Python libraries.
Architectural Background
Currently, it’s straightforward to share “static” (i.e. non-callback-bound) components across apps or projects. Simply define write a Python function that takes some parameters returns a nested component:
my_components.py
def SparkLine(y, label):
return html.Div([
html.Div(label),
dcc.Graph(figure=px.line(y=y).update_axes(showline=False)
])
app.py
import my_components
app.layout = html.Div([
SparkLine([1, 2, 3, 1, 3, 1], 'Returns')
])
However, if your component contains a callback then you will run into two issues:
- Circular callbacks -
my_components
importsapp
andapp
importsmy_components
. - Static IDs - If your callbacks have static IDs, then you can’t embed multiple
SparkLine
components.
There have been many proposals over the years to address these two issues, including Object Oriented Components or embedding functions directly in the layout.
The key architectural constraint that has limited this proposals in the past is Dash’s scalable stateless architecture. Dash’s backend is designed to be able to run as multiple “copies” across multiple processes (“workers”) or servers. Requests from the front-end should be able to hit any of the “copies” of the running Dash app and return the same result. This means that any proposal that modifies data on the backend without passing it up to the front-end (or publishing to a shared data store like Redis) is a no-go. This is the heart of the constraint that “all callbacks must be defined in advance”.
The proposal above gets around these constraints by:
- Using a global import of the app object:
dash.g.app
- Using pattern-matching callbacks with sufficiently unique yet accessible IDs
- Defining the callbacks in advance by registering them as a “side-effect” of importing the component library file
What’s Next
- We would love to hear any feedback that you have on this design
-
dash.g.app
is not yet available in Dash. We need some more feedback on this API and it needs to pass review. - We need to investigate APIs for sharing “sharing intermediate results”
- We need a good name for these types of components. Some options: “Serverside Components”, “Higher Order Components”, “Callback-Bound Components”. Any other ideas?
Once the are published, we will create some of these serverside components as part of the dash_table
and dcc
libraries.
Thanks for reading!
PS - This R&D effort was graciously sponsored by a community member through our Sponsored Open Source Development Program. Many thanks to this sponsor