Writing Dash Plugins using Dash Hooks

New in Dash 3.0

Dash provides a system of hooks for creating installable plugins that can extend the functionality of Dash apps and that can be used across multiple apps. You can use hooks to add to an app’s layout, add callbacks, routes, or error handlers.

Tutorial

This tutorial teaches the main features of Dash hooks.

In this tutorial, you’ll build a plugin that displays a notification when a callback error happens in a Dash app. The notification will explain what type of errors will be shown the first time the app loads. It will also include a button so it can be closed by the app user. It will reappear each time a callback error happens.

Create the Project Structure

In this tutorial, we’ll build a reusable module. To create the project structure:

  1. Create a folder for the project and within that folder, create a file called pyproject.toml.
  2. Within the same folder, create a subfolder called callback_error_plugin.
  3. In the callback_error_plugin subfolder, create a file called __init__.py.
callback_error_plugin/
└── __init__.py
pyproject.toml

<img>

The pyproject.toml file is for project metadata, while callback_error_plugin is where the main logic of the module will live in the __init__.py file.
You could also split the module into multiple files, with __init__.py as the entry point and a new file callback_error_plugin.py for the main logic.

Write the Plugin Code in __init__.py

  1. Open the __init__.py file and import html, hooks, set_props, Input, and Output from dash to use later. Then, add the component that will be displayed when an error happens. Use dash.html components to create a simple banner that will be displayed when there’s a callback error.

    ```python
    from dash import html, hooks, set_props, Input, Output

    def generate_error_notification():
    return [
    html.Div(
    [
    html.Div(
    [
    html.Span(
    “Callback errors will display here.”,
    id=”error-text”,
    ),
    html.Button(
    “×”,
    id=”dismiss-button”,
    style={
    “position”: “absolute”,
    “top”: “5px”,
    “right”: “10px”,
    },
    ),
    ],
    style={
    “padding”: “15px”,
    “border”: “1px solid #f5c6cb”,
    },
    )
    ],
    id=”callback-error-banner-wrapper”,
    )
    ]
    ```

  2. Create a function add_error_notifications that accepts one argument error_text. This function will be called by apps that use the plugin’s functionality. App developers will be able to pass custom error text to it.

    python def add_error_notifications(error_text="There was an error"):

  3. In the newly created add_error_notifications function, use the hooks.layout decorator to add the component to the app layout.

    python @hooks.layout() def update_layout(layout): return generate_error_notification() + (layout if isinstance(layout, list) else [layout])

  4. In the add_error_notifications function, use the hooks.callback decorator to add a callback that handles dismissing the notification.

    python @hooks.callback( Output("callback-error-banner-wrapper", "style"), Input("dismiss-button", "n_clicks"), prevent_initial_call=True, ) def hide_banner(n_clicks): if n_clicks: return dict(display="none")

  5. In the add_error_notifications function, use the hooks.error decorator to add a callback error handler. This error handler function unhides the banner and also update its text to show the actual error that occurred:

    python @hooks.error() def on_error(err): set_props("callback-error-banner-wrapper", dict(style=dict(display="block"))) set_props("error-text", dict(children=f"{error_text}: {err}"))

View the full code

from dash import html, hooks, set_props, Input, Output


def generate_error_notification():
    return [
        html.Div(
            [
                html.Div(
                    [
                        html.Span(
                            "Callback errors will display here.",
                            id="error-text",
                        ),
                        html.Button(
                            "×",
                            id="dismiss-button",
                            style={
                                "position": "absolute",
                                "top": "5px",
                                "right": "10px",
                            },
                        ),
                    ],
                    style={
                        "padding": "15px",
                        "border": "1px solid #f5c6cb",
                    },
                )
            ],
            id="callback-error-banner-wrapper",
        )
    ]

def add_error_notifications(error_text="There was an error"):
    @hooks.layout()
    def update_layout(layout):
        return generate_error_notification() + (
            layout if isinstance(layout, list) else [layout]
        )

    @hooks.callback(
        Output("callback-error-banner-wrapper", "style"),
        Input("dismiss-button", "n_clicks"),
        prevent_initial_call=True,
    )
    def hide_banner(n_clicks):
        if n_clicks:
            return dict(display="none")

    @hooks.error()
    def on_error(err):
        set_props("callback-error-banner-wrapper", dict(style=dict(display="block")))
        set_props("error-text", dict(children=f"{error_text}: {err}"))

Configure a pyproject.toml File

Add the following to the pyproject.toml file. Information in pyproject.toml is used when the package is built (the next step).

[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "callback_error_plugin"
version = "0.0.1"
dependencies = [
    "dash>=3.0.3",
]
[tool.setuptools]
packages = ["callback_error_plugin"]

Build the Plugin

To build an installable plugin, use build.

In a virtual environment:

pip install build

To build the plugin, run:

python -m build

A Python wheel and source distribution are generated in a dist directory alongside the plugin code.

Install and Test the Plugin

  1. Create and activate a new virtual environment.
  2. Install dash>=3.0.3.
  3. Install the wheel or source distribution created in the earlier “Build the Plugin” section with pip install &lt;path-to-generated-dist&gt;.
  4. Create an app with the following code in a file called app.py to use with the new plugin. Note, the app imports add_error_notifications from the plugin package and calls it with a custom error message:
    ```python
    from dash import dcc, html, callback, Input, Output, Dash
    from callback_error_plugin import add_error_notifications

    add_error_notifications(“Here is the error message”)

    app = Dash()

    app.layout = html.Div([
    dcc.Input(id=’input-number’, type=’number’, value=1),
    html.Div(id=’output-div’)
    ])

    @callback(
    Output(‘output-div’, ‘children’),
    Input(‘input-number’, ‘value’)
    )
    def update_output(value):
    result = 10 / value
    return f’The result is {result}’

    if name == ‘main’:
    app.run(debug=True)
    `` 5. Run the app withpython app.pyand try it. This example app will throw a division by zero error when0` is entered in the input.

    <img>.

Tip: You can also publish the built package to PyPI, the official repository of packages for Python. See the Python Packaging User Guide for details on uploading the generated wheel and source distributions.

Available Hooks

Dash’s hooks system supports the following hooks:

Adding to an App’s Layout with hooks.layout

Use dash.hooks.layout as a function decorator to update an app’s layout before the Dash app is served. The decorated function receives the existing layout as its first argument. What’s returned from the decorated function will be the app’s new layout.

Here’s an example that adds a new html.Div to an existing layout.

from dash import hooks, html

@hooks.layout()
def update_layout(layout):
    return [html.Div("New layout content")] + layout

Adding Callbacks with hooks.callback

Use the hooks.callback decorator to add callbacks to an app. It accepts the same parameters as app.callback and dash.callback.

Here’s an example of adding a callback using hooks.callback. This callback outputs a message when a button is clicked.

from dash import hooks, Input, Output

@hooks.callback(
    Output("message-container", "children"),
    Input("app-button", "n_clicks"),
    prevent_initial_call=True,
)
def output_message(n_clicks):
    if n_clicks:
        return "Displaying a message in the app on click"

Callbacks added with hooks.callback can reference components added with hooks.layout. See the “Example: Creating a Plugin” section for an example of using these hooks together.

Hiding Callbacks from Devtools

New in Dash 3.3

We recommend setting hidden=True on a callback to hide it from the callback graph in the dev tools UI. This allows plugin users to clearly see their own app’s callbacks when debugging, without the plugin’s internal callbacks getting in the way.

from dash import hooks, Input, Output

@hooks.callback(
    Output("message-container", "children"),
    Input("app-button", "n_clicks"),
    prevent_initial_call=True,
    hidden=True
)
def output_message(n_clicks):
    if n_clicks:
        return "Displaying a message in the app on click"

Adding Flask Routes with hooks.route

Use the hooks.route decorator to add Flask routes to an app. The function that hooks.route decorates is the view function for the route.

The decorator has the following arguments for configuring the route:

The following example serves some JSON data at /get-data:

from flask import jsonify
from dash import hooks

@hooks.route(methods=("GET",), name="get-data")
def new_route():
    data = {
        "status": "success"
    }
    return jsonify(data)

Adding an Error Handler Function with hooks.error

Use hooks.error as a function decorator to add a callback error handler. The decorated function will be set as the error handler for any app that uses the plugin. The decorated function will receive the error that occurs in a callback as an argument, err.

from dash import hooks

@hooks.error()
def on_error(err):
    print(f"Printing the error to the console: {err}")

See the callback error handlers page for more details on creating callback error handlers.

Updating App Properties with hooks.setup

Use hooks.setup as a function decorator to get a reference to an app’s instantiated dash.Dash app object. This can be used to update properties of the app.

Here’s an example that adds text to the app title:

from dash import hooks

@hooks.setup()
def setup(app_object):
    app_object.title = f"{app_object.title} | Appending to the app title"

See the app object section in the reference docs for a list of properties that can be set on the app object.

Updating an App’s HTML with hooks.index

hooks.index allows you to access a Dash app’s HTML and alter it. Use hooks.index as a decorator. The decorated function will receive the app’s index, a string, as its only argument. In this example, an HTML element is inserted just after the start of the &lt;body&gt; tag.

from dash import hooks

@hooks.index()
def update_index(app_index):
    body = "&lt;body&gt;"
    body_idx = app_index.find(body) + len(body)
    updated_app_index = app_index[:body_idx] + '&lt;div&gt;Added content&lt;div&gt;' + app_index[body_idx:]
    return updated_app_index

Making Custom Data Available in a Callback

Use the custom_data hook as a decorator to make custom data available to callback_context within an app’s callbacks.

from dash import hooks

@hooks.custom_data("custom_data_name")
def custom_data_func(_ctx):
    # The current callback context (here as the parameter `_ctx`) is available
    # And could be used when generating custom data to return from this function
    return "some custom data"

Within a Dash app, this value can be accessed from dash.callback_context:

from dash import dash, callback, Input, Output

@callback(
    Output('cities-radio', 'value'),
    Input('cities-radio', 'options'))
def set_cities_value(available_options):
    custom_value = dash.callback_context.custom_data["custom_data_name"] # will return "some custom data"
...

Adding Components to Dev Tools UI with hooks.devtool

Use hooks.devtool to add custom components to the Dash dev tools UI. Components added with this hook are rendered in the dev tools panel when dev_tools_ui=True is set on the Dash app (the default in debug mode).

hooks.devtool accepts the following arguments:

The following example adds a simple component to the dev tools UI:

from dash import hooks

hooks.devtool(
    namespace="dash_html_components",
    component_type="Div",
    props={"children": "Custom plugin dev tools panel"}
)

To use the component in callbacks, give it an id and use hooks.callback with optional=True on the callback:

from dash import hooks, Input, Output

hooks.devtool(
    namespace="dash_html_components",
    component_type="Div",
    props={
        "children": [
            {
                "namespace": "dash_html_components",
                "type": "Button",
                "props": {
                    "id": "plugin-button",
                    "children": "Click me"
                }
            },
            {
                "namespace": "dash_html_components",
                "type": "Div",
                "props": {
                    "id": "plugin-output",
                    "children": "Output will appear here"
                }
            }
        ]
    }
)

@hooks.callback(
    Output("plugin-output", "children"),
    Input("plugin-button", "n_clicks"),
    # `optional=True` is required because devtools are outside the regular layout
    optional=True,
    prevent_initial_call=True
)
def handle_plugin_button(n_clicks):
    return f"Button clicked {n_clicks} times"

You can get the values for namespace, component_type and props by calling to_plotly_json() on a Dash component:

component = dash.html.Div("Custom plugin dev tools panel")
component.to_plotly_json()
# Output of component.to_plotly_json()
{'props': {'children': 'Custom plugin dev tools panel'}, 'type': 'Div', 'namespace': 'dash_html_components'}
# Values from to_plotly_json() passed to `hooks.devtool`. Note: `type` on the `to_plotly_json` output corresponds to the `component_type` argument on `hooks.devtool`:
hooks.devtool(
    namespace=component.to_plotly_json()['namespace'],
    component_type=component.to_plotly_json()['type'],
    props=component.to_plotly_json()['props'],
)

Using Custom React Components

As well as rendering Dash components, hooks.devtool can render custom React components.

To render a React component in the dev tools UI, the component needs to be available in the window namespace.

Here’s a React component in custom_devtools.js:

window.CustomDevToolsLib = {
    SimpleComponent: function(props) {
        return window.React.createElement('div', { style: { padding: '10px' } }, [
            window.React.createElement('h4', { key: 'title' }, props.title || 'Custom Component'),
            window.React.createElement('p', { key: 'msg' }, props.message || 'Hello!')
        ]);
    }
};

Which is then made available to the Dash application by adding it with hooks.script and then adding it to the dev tools UI with hooks.devtool:

from dash import hooks

hooks.script([{
    "dev_package_path": "custom_devtools.js", # Relative path within package
    "namespace": "custom_devtools_plugin",
    "dev_only": True  # Only load in debug mode
}])

hooks.devtool(
    namespace="CustomDevToolsLib",
    component_type="SimpleComponent",
    props={
        "title": "My Custom Component",
        "message": "This is a custom React component in devtools!"
    },
    position="left"
)

Dev Tools Styling

New in Dash 3.3.0

When creating custom React components, use the devtool component API to apply styles that are consistent with the styling of built-in dev tools UI buttons.

useDevtool() - Manages which popup is open.

const { popup, setPopup } = window.dash_component_api.devtool.useDevtool();
const isOpen = popup === 'my-popup-id';
setPopup('my-popup-id');  // Open this popup
setPopup('');             // Close popup by setting it to ''

useDevtoolMenuButtonClassName(popupId) - Returns a CSS class string that styles a button to match built-in devtools buttons, ensuring that it appears as toggled correctly based on the popup state.

const className = window.dash_component_api.devtool.useDevtoolMenuButtonClassName('my-popup-id');

// Use it on a button and it will automatically show toggled state when popup is open
window.React.createElement('button', { className: className }, 'My Component')

Setting Hook Priority

If you are using multiple hooks of the same type, you can configure the order they run in using priority. Higher numbers run first. If no order is configured, the order is not guaranteed.

...
@hooks.layout(priority=2)
    def update_layout(layout):

Adding Clientside Callbacks and External Scripts and CSS

The dash.hooks module also allows you to directly add to an app’s clientside callbacks, scripts and CSS stylesheets.

Adding Clientside Callbacks with hooks.clientside_callback

You can add clientside callbacks to Dash apps using hooks.clientside_callback. For example, here’s the earlier callback example rewritten as a clientside callback:

from dash import hooks, Input, Output

hooks.clientside_callback(
    """
    function(n_clicks) {
        if (n_clicks) {
            return {"display": "none"};
        }
        return {};
    }
    """,
    Output("callback-error-banner-wrapper", "style"),
    Input("dismiss-button", "n_clicks"),
    prevent_initial_call=True
)

hooks.clientside_callback has the same functionality as app.clientside_callback.

Adding External JavaScript Scripts with hooks.script

Use hooks.script to add JavaScript scripts to apps. hooks.script takes a list of dicts, where each dict specifies one script to add. Scripts can be loaded from external URLs or package paths. The following example adds an external script:

from dash import hooks

hooks.script([{'external_url': 'https://www.google-analytics.com/analytics.js', 'external_only': True}])

Script Options

Each script dictionary can include the following fields:

Examples

Always use external URL:

hooks.script([{
    'external_url': 'https://cdn.example.com/library.js',
    'external_only': True
}])

Use package path:

__version__ = "1.0.0"

hooks.script([{
    'external_url': 'https://cdn.example.com/my-plugin.js',
    'relative_package_path': 'plugin_resources/my-plugin.js',
    'namespace': 'my_plugin_package'
}])

Adding External Stylesheets with hooks.stylesheet

Use hooks.stylesheet to add stylesheets to apps. hooks.stylesheet takes a list of dicts, where each dict specifies one stylesheet to add. Stylesheets can be loaded from external URLs or package paths. The following example adds an external stylesheet:

from dash import hooks

hooks.stylesheet([{'external_url': 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css', 'external_only': True}])

Stylesheet Options

Each stylesheet dictionary can include the following fields:

Examples

Always use external URL:

hooks.stylesheet([{
    'external_url': 'https://cdn.example.com/styles.css',
    'external_only': True
}])

Use package path:

__version__ = "1.0.0"

hooks.stylesheet([{
    'external_url': 'https://cdn.example.com/my-plugin.css',
    'relative_package_path': 'plugin_resources/my-plugin.css',
    'namespace': 'my_plugin_package'
}])

Automatic Plugin Discovery

The earlier tutorial demonstrated creating a plugin that could be used with a Dash app by importing it to register the plugin’s hooks. It’s also possible to create a plugin where hooks are automatically discovered and registered for a Dash app once the plugin is installed.

For automatic discovery, the plugin needs to define a dash_hooks entry point. When a plugin that defines a dash_hooks entry point is installed in an environment where a Dash app is run, Dash will run the hooks defined by the plugin before serving the Dash app.

Here’s an example of a pyproject.toml file for the plugin from the earlier tutorial:

[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "callback_error_plugin"
version = "0.0.2"
dependencies = [
    "dash>=3.0.3",
]

[project.entry-points."dash_hooks"]
callback_error_plugin = "callback_error_plugin"

[tool.setuptools]
packages = ["callback_error_plugin"]

With the entry point configured, the code in the plugin’s __init__.py file would look like this, with the calls to hooks defined outside of the function:

from dash import html, hooks, set_props, Input, Output


def generate_error_notification():
    return [
        html.Div(
            [
                html.Div(
                    [
                        html.Span(
                            "Callback errors will display here.",
                            id="error-text",
                        ),
                        html.Button(
                            "×",
                            id="dismiss-button",
                            style={
                                "position": "absolute",
                                "top": "5px",
                                "right": "10px",
                            },
                        ),
                    ],
                    style={
                        "padding": "15px",
                        "border": "1px solid #f5c6cb",
                    },
                )
            ],
            id="callback-error-banner-wrapper",
        )
    ]

@hooks.layout()
def update_layout(layout):
    return generate_error_notification() + (
        layout if isinstance(layout, list) else [layout]
    )

@hooks.callback(
    Output("callback-error-banner-wrapper", "style"),
    Input("dismiss-button", "n_clicks"),
    prevent_initial_call=True,
)
def hide_banner(n_clicks):
    if n_clicks:
        return dict(display="none")

@hooks.error()
def on_error(err):
    set_props("callback-error-banner-wrapper", dict(style=dict(display="block")))
    set_props("error-text", dict(children=f"The error is: {err}"))

If multiple plugins in the environment where a Dash app is run have a dash_hooks entry point defined, each plugin’s defined hooks will be run before serving the Dash app.