đź“Ł Modular & Reusable Python Components (With Attached Callbacks)

:wave: 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:

  1. from dash.g import app - This is proposed and not yet published to Dash. Here’s the PR. This import allows us to define an app.callback without needing to import app from app.py
  2. 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.
  3. @app.callback - Callbacks need be defined in advance. By defining @app.callback within the class but outside an __init__ method, this @app.callback will be called when the component is imported (from chriddyp_components import MarkdownWithColor)
  4. class MarkdownWithColor - We’re using a class just to keep things modular. We’re not using any object oriented features here. This class allows us to define things like MarkdownWithColor.ids.dropdown without polluting the file’s namespace. This allows you to write multiple components within the same file.
  5. **(dropdown_options if dropdown_options is not None else {}) - This line passes allows component consumers to override dropdown options as they see fit. This inline if expression just keeps things on a single line and is just my own convention / preference for keeping code terse.
  6. Pattern Matching IDs - The ID would get evaluated as:
    {
          'component': 'chriddyp_components.MarkdownWithColor',
          'subcomponent': 'dropdown',
          'instance': <some-long-UUID-expression>
    }
    
    The keys (component, subcomponent, and instance) 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 uses MATCH. An alternative name here would be id, but then we have id={'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 the ids 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:

  1. Circular callbacks - my_components imports app and app imports my_components.
  2. 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:

  1. Using a global import of the app object: dash.g.app
  2. Using pattern-matching callbacks with sufficiently unique yet accessible IDs
  3. 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 :bowing_man:

4 Likes

First, I think this kind of functionality would be a great addition to Dash. And while the proposed approach is quite elegant, I can’t help to think if it wouldn’t be a better approach to go down a route more similar to flask blueprints?

That was one of the main sources of inspiration for the DashProxy object in dash-extensions, and while it provides more-or-less the same benefits of the approach proposed above, it has a few other advantages too,

  • Since there is virtually no difference in syntax between writing a component and an app, the barrier of entry for developing custom components is low (if you can write a Dash app, you can write a custom component)
  • It would allow a natural extension to more advanced functionality of custom components, e.g. using client side callback, linking of external stylesheets/javascript, etc. using existing syntax
  • It would be possible to “mount” existing apps as a components, which can be useful in many cases; I have used this approach in e.g. http://dash-leaflet.herokuapp.com/ to make interactive examples
  • This approach would also enable multipage application very elegantly; simply create each page as you would create an app, and mount the pages in the main app

My main concern about creating reusable components in Python (including callbacks) is performance. In my experience, the performance of a Dash application remains decent as long as you don’t have too many components and/or rely too much on “normal callbacks” (client side callbacks are performant of course). Making it possible to write large components in Python (in number of subcomponents/callbacks, e.g. say you created tree selection component in Dash instead of porting a React component) that can easily be reused may pave the way for creating poorly performing applications. This concern is more fundamental I believe, and I don’t see any immediate way to address it apart from documentation, i.e. making it clear what Dash is good at, and what Dash is not so good at.

4 Likes

Hey @chriddyp thanks for sharing the proposal!

Here are a few thoughts and questions:

Is this in addition to or instead of the Component Plugin in Dash Labs? Could you say more about the pros and cons of each?

In Example 1.6, I think the name instance is confusing. If it looks like and id and acts like an id maybe it should be called an id even if id feels redundant.

I’m a fan of anything that simplifies the API and/or makes it easier use. I’m interested to hear your thoughts about @Emil’s suggestion of flask blueprints.

Emil also brought up important concerns about performance. I expect this new functionality could improve performance by making it easier to share expensive intermediate results, use other Python libraries such HoloViews, or as demonstrated in the Dash Labs DataTable plugin, use clientside functions. Also, if one of the purposes is to take common Dash use cases and make them modular, then I expect it wouldn’t change performance – it would just make those things easier to do. I think making new custom components in React is a separate topic and the differences could be covered in the documentation.

For a name, what do you think about Composite Components?

This would likely be instead of. The Component Plugin in Dash Labs has two shortcomings:

  1. Consumers of components are required to learn the templates API. I would prefer to have a solution that looks and feels like regular dash components.
  2. Since templates are tied to callbacks, and callbacks can only be defined in advance, template-driven plugins can’t be used in a dynamic context (i.e. defined within a callback). This limits their use for multipage apps, multitab apps, dcc.Interval, etc

One key difference between components and apps is the ability to pass parameters into the component. I do like the idea of blueprints for things like multi-page apps, but for components I think we need something more flexible.

Eventually I think we could have both, and reserve blueprints mainly for the multi-page app use case.


Now, an API is only as good as its documentation! I’m imagining that we would write documentation that would start with the simple use cases and slowly build up. Something like this:

  1. Static Component
    Improve the organization of your code by assigning chunks of your layout as variables and moving those variables into other files.

    As projects get larger, we recommend creating a components/ directory.

    Here is a simple example. Move your Header into its own component.

    Start with this:
    app.py

    app.layout = html.Div([
          html.H1('Weekly Analytics')  # In practice, refactor large component chunks not just 1-liners
    ])
    

    Refactor into this:
    components/Header.py

    Header = html.H1('Header')
    

    components/__init__.py

    from .Header import Header
    

    app.py

    from components import Header
    app.layout = html.Div([
        Header
    ])
    
  2. Static Component with Callbacks
    In Multi-Page Apps, your layout chunks will often have their own callbacks associated with them. In our experience, keeping layout chunks in the same file as the callbacks keeps things well organized as the interactivity is often tightly coupled to the UI.
    Use from dash.g import app to prevent circular callbacks.
    pages/Historical.py

    from dash.g import app
    
    def Historical():  # Or Historical = html.Div([... if there isn't anything dynamic or configurable in this chunk
        return html.Div([
            html.H1('Historical Analysis'),
            dcc.Dropdown(id='historical-dropdown'),
            dcc.Graph(id='historical-graph'),
        ])
    
    @app.callback(Output('historical-graph', 'figure'), Input('historical-dropdown', 'value'))
    def update_graph(value):
        ...
    

    app.py

    from pages import Historical
    
    app.layout = html.Div([
        dcc.Location(id='url'),
        html.Div(id='content'),
    ])
    
    @app.callback(Output('content', 'children'), Input('url', 'href')):
    def update(href):
        path_id = app.strip_relative_path(href)
        if path_id == 'historical':
            return Historical()
    
  3. Reusable Components
    Sometimes your layout chunks will have certain parameters. Perhaps your app uses the same type of line chart throughout. Alternatively, your component might pull real-time data when it is rendered. In either case, you may benefit from refactoring your code by converting similar layout chunks into its own function with parameters:
    components/LineGraph.py

    def LineGraph(x_name, y_name):
        df = get_data()
        fig = px.scatter(df, x=x_name, y=y_name)
        return dcc.Graph(figure=fig)
    

    app.py

    app.layout = html.Div([
        LineGraph('time', 'velocity'),
        LineGraph('velocity', 'temperature'),
    ])
    
  4. Reusable Components with Callbacks
    If your reusable functional component requires a callback, then use Pattern-Matching Callbacks to define generic callbacks in advance.

    If your callbacks are configurable, then pass that parameter into a dcc.Store. Here, df is passed from the component to the Store to the callback.

    components/TabbedGraph.py

    from dash.g import app
    
    def TabbedGraph(df):
        instance = str(uuid.uuid4())
        return html.Div([
            dcc.Store(
                id={'component': 'TabbedGraph', 'subcomponent': 'Store', 'instance': uid},
                data={'records': df.to_dict('records')}
            ),
            dcc.Tabs(
                id={'component': 'TabbedGraph', 'subcomponent': 'Tabs', 'instance': uid},
                children=[
                    dcc.Tab('Graph', value='Graph', label='Graph', id={'component': 'TabbedGraph', 'subcomponent': 'Graph', 'instance': uid}),
                    dcc.Tab('Data', value='Data', label='Data', id={'component': 'TabbedGraph', 'subcomponent': 'Data', 'instance': uid})
                ],
            ),
            html.Div(id={'component': 'TabbedGraph', 'subcomponent': 'Content', 'instance': uid})
        ])
    
    @app.callback(
        Output({'component': 'TabbedGraph', 'subcomponent': 'Content', 'instance': MATCH}, 'children'),
        Input({'component': 'TabbedGraph', 'subcomponent': 'Tabs', 'instance': MATCH}, 'value'),
        Input({'component': 'TabbedGraph', 'subcomponent': 'Store', 'instance': MATCH}, 'data'),
    )
    def update_tab(value, records):
        df = pd.DataFrame(records)
        if value == 'Graph':
            return dcc.Graph(figure=px.scatter(df, x='time', y='voltage'))
        else:
            return dash_table.DataTable(data=records)
    

    app.py

    from components import TabbedGraph
    
    df1 = ...
    df2 = ...
    
    app.layout = html.Div([
        TabbedGraph(df1),
        TabbedGraph(df2),
    ])
    

    You could make this code more DRY (“Don’t Repeat Yourself”) & concise by moving into a class, retrieving "TabbedGraph" from the class name, declaring the IDs as a variable. The usage would remain the same:

    from dash.g import app
    
    class TabbedGraph:
        component_name = __qualname__ # Evaluates to TabbedGraph
        
        class ids:
            store = lambda instance: ({
                'component': component_name,
                'subcomponent': 'Store',
                'instance': instance
            })
            tabs = lambda instance: ({
                'component': component_name,
                'subcomponent': 'Graph',
                'instance': instance
            })
            content = lambda instance: ({
                'component': component_name,
                'subcomponent': 'Content',
                'instance': instance
            })
    
        @app.callback(
            Output(ids.content(MATCH), 'children'),
            Input(ids.tabs(MATCH), 'value'),
            Input(ids.store(MATCH), 'data'),
        )
        def update_tab(value, records):
            df = pd.DataFrame(records)
            if value == 'Graph':
                return dcc.Graph(figure=px.scatter(df, x='time', y='voltage'))
            else:
                return dash_table.DataTable(data=records)
    
        def __init__(df):
            instance = str(uuid.uuid4())
            return html.Div([
                dcc.Store(
                    id=ids.Store(instance),
                    data={'records': df.to_dict('records')}
                ),
                dcc.Tabs(
                    id=ids.Tabs(instance),
                    children=[
                        dcc.Tab('Graph', value='Graph', label='Graph'),
                        dcc.Tab('Data', value='Data', label='Data')
                    ],
                ),
                html.Div(id=ids.content(instance))
            t)
    s``
    
    
  5. Providing an Extensible API for Reusable Components

    If you’re publishing your component to other users or the community, you may consider exposing a few parameters to make its usage as flexible as possible. We recommend:

    1. Exposing the IDs of the inner components & allowing users to pass in their own ID so that they can attach their own callbacks
    2. Passing along arbitrary keyword arguments to the inner components.

    This is done by:

    • Exposing <Component>.ids as above with the class ids:
    • Modifying __init__(df) to take an instance= argument for custom IDs: def __init__(df, instance=None)
    • Modifying __init__(df) to add store_props=None, tabs_props=None, and content_props=None. Refer to the inner components using the same names as the IDs.
    • Use the same inner component names in the keyword arguments and the IDs for consistency (e.g. store, content, tabs).

    This would allow usage like:

    app.py

    from components import TabbedGraph
    
    df1 = ...
    df2 = ...
    
    app.layout = html.Div([
        TabbedGraph(df1, instance='tab-1', tabs_props={'parent_className': 'custom-tabs'}),
        html.Div(id='tab-1-output')
    ])
    
    @app.callback(
        Output('tab-1-output', 'children'),
        Input(TabbedGraph.ids.Tabs('tab-1'), 'value')
    )
    def update_children(tab):
        if tab == 'Graph':
            return 'More info about the graph...'
        else:
            return 'More info about the table...'
    

    The complete example of the component would be:

    from dash.g import app
    
    class TabbedGraph:
        component_name = __qualname__ # Evaluates to TabbedGraph
        
        class ids:
            store = lambda instance: ({
                'component': component_name,
                'subcomponent': 'Store',
                'instance': instance
            })
            tabs = lambda instance: ({
                'component': component_name,
                'subcomponent': 'Graph',
                'instance': instance
            })
            content = lambda instance: ({
                'component': component_name,
                'subcomponent': 'Content',
                'instance': instance
            })
    
        @app.callback(
            Output(ids.content(MATCH), 'children'),
            Input(ids.tabs(MATCH), 'value'),
            Input(ids.store(MATCH), 'data'),
        )
        def update_tab(value, records):
            df = pd.DataFrame(records)
            if value == 'Graph':
                return dcc.Graph(figure=px.scatter(df, x='time', y='voltage'))
            else:
                return dash_table.DataTable(data=records)
    
        def __init__(df, instance=None, store_props=None, tabs_props=None, content_props=None):
            if instance is None:
                instance = str(uuid.uuid4())
            return html.Div([
                dcc.Store(
                    id=ids.Store(instance),
                    data={'records': df.to_dict('records')},
                    **(store_props if store_props is not None else {})
                ),
                dcc.Tabs(
                    id=ids.Tabs(instance),
                    children=[
                        dcc.Tab('Graph', value='Graph', label='Graph'),
                        dcc.Tab('Data', value='Data', label='Data')
                    ],
                    **(tabs_props if tabs_props is not None else {})
                ),
                html.Div(
                    id=ids.content(instance),
                    **(content_props if content_props is not None else {})
                )
            ])
    
  6. Passing Parameters into the Callback via Redis
    If your data is too large for the network or the client, you may consider storing this data in a memory database that is shared across processes like Redis

    OK I’m running out of steam today, but here would be another example…

  7. Exposing Intermediate Computations

    Your component may perform expensive computations within a callback that could be useful to other components or callbacks. You can expose these computations with ServersideOutput

    As above, but here would be another example…


Well said, and agreed!


Yeah this is a really tricky part of the API.

One thing I forgot to mention is that instance doesn’t completely act like an ID.

The main difference is that you can’t create a callback that updates the component’s properties like you could with a proper Dash component.

For example, you cannot do this:
app.py

app.layout = html.Div([
    dcc.Dropdown(id='dropdown', options=...),
    TabbedGraph(instance='tabbed-graph-1')
])

@app.callback(Output('tabbed-graph-1', 'df'), Input('dropdown', 'value')):
def update_tabbed_graph(dataset):
   if dataset == 'cars':
       return px.data.cars()
   elif dataset == 'iris':
       return px.data.iris()

However, you could do this:
app.py

app.layout = html.Div([
    dcc.Dropdown(id='dropdown', options=...),
    TabbedGraph(instance='tabbed-graph-1')
])

@app.callback(Output(TabbedGraph.ids.store('tabbed-graph-1'), 'data'), Input('dropdown', 'value')):
def update_tabbed_graph(dataset):
   if dataset == 'cars':
       return px.data.cars().to_dict('records')
   elif dataset == 'iris':
       return px.data.iris().to_dict('records')

So, having a property that distinguishes this these “composite components” from a Dash component would be important. If we had TabbedGraph(id='tabbed-graph-1') then it could make it even more confusing that you couldn’t address callbacks to this component.

But I’m flexible on the name instance.

Yeah that’s pretty good! Matches the dictionary definition pretty well:

  • adj. Made up of distinct components; compound.
  • adj. Made by combining two or more existing things, such as photographs.

Other ideas:

  • Functional Component - A little too overlapping with React’s functional components
  • Component Plugin - We’re not really plugging in anything to components, and there is already the dash plugin= property
  • Higher Order Component - Another thing taken from React’s vocabulary. Similar intention: “A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.”

I could imagine “instance” being related to these, like:

  • composite_id
  • functional_id
  • hoc_id
  • composite_instance
  • functional_instance
  • hoc_instance

Thanks so much for reading & the feedback :slight_smile:

2 Likes

First of all, I love the approach due to how easy it is to read and understand. Conceptually, it’s very clear too.

One suggestion: please allow the possibility to represent the composite components as class instances. That has become pretty important for us to develop somewhat complex UIs, which require considering different shapes and functionalities.

Thanks for spending time to working on this. I’ve found it’s a much needed functionality.

1 Like

Thanks @carloslang!

Could you describe more about this? Does the example above with class TabbedGraph accomplish what you are looking for or something beyond this?

Thanks, @chriddyp. You have written down some very concise thoughts and I like the approach a lot.

At the company I work for, we have been building large dashboards that reuse a lot of logic for two years, now. In this process, we have needed something similar and built a prototype that fits our needs. It is in no way as thought and general out as your solution.

I just wanted to add some of our problems and our solutions:

IDs for components

One problem that you have already described is IDs for components need to be unique throughout the application and sometimes you have to reference components from other components or you have multiple components of the same type / class in the same application.

We have found the component ID store solution to work great. (Our implementation used a simple dict that mapped a component-unique identifier to an application-unique identifier, so basically the same thing you proposed.)

Callbacks

Callbacks for components are somewhat of a problem since we need to register all callbacks at the start of the application and also the application object needs to be present to register them. Our solution tackled another problem we had: integrating our own components into the dash application layout. What we wanted to do is:

app.layout = html.Div([
  html.H1("My own component."),
  MyCoolComponent("This is a title for this component.")
])

I am not too deep into the inner workings of the provided components like html.H1 but the easy solution for us was to return the native dash components of our own components using a method call. We then did:

app.layout = html.Div([
  html.H1("My own component."),
  MyCoolComponent("This is a title for this component.").render(app)
])

This additionally opened the possibility to chain method calls like MyCoolComponent().add_loading_indicator().render(app).

Callbacks were then automatically registered the moment the component got rendered into the application. This was achieved by moving all callbacks into a single method (something like register_callbacks(app)) that was called when the component was rendered. This way, callbacks for different instances were registered at the right time and multiple times.

Callbacks for app layout functions

I remember vaguely that sometimes we needed to be careful when the application layout was a function as to not register the callbacks multiple times. I would need to further look into the code to find out what we did with that. But that obviously is an implementation detail something to keep in mind.

Conclusion

As for the name, “component” will probably confuse people. A traditional component in Dash contains React code, a structure for its data and then you need to hook into its interactions (callbacks). Here, we are talking about making it easier to refactor parts of your app layout and reuse them in different places. (Of course, this can also mean reusing frontend code like client-side callbacks.)

Maybe we can try to think about some other layout / template systems and find some name there:

  • In jinja2 you can use macros to be able to use parametrized layout code in different places. I kind of like the name here since it is so different from component, and is usually connected to a collection of commands (native components) that get executed (rendered) together.
  • As for React, I do not particularly like their naming as it is very hard to understand if you do not have a deeper understanding of React. (Don’t get me wrong: Once you get into the React ecosystem, it is concise and clear. But from the outside hard to understand.)
  • Other ideas that come to my mind are: module, section, composite, collection.

Overall, I love the idea and the effort. We will definitely be able to make use of this and deeper dash integration for these kind of components would be handy in many instances. In my eyes, a component system in the backend that makes it possible to reuse layout and callback code is very powerful.

3 Likes