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. Give this hook a priority of 1. This ensures that layout is updated first, adding the new component. The new component can then be used in a callback hook in the next step.

    python @hooks.layout( priority=1 ) 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(priority=1)
    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 can be configured in the same way 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"

You’ll need to also have a hooks.layout hook to use hooks.callback. See the “Example: Creating a Plugin” section for an example of using these hooks together.

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 + 1]
    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"
...

Setting Hook Priority

If you are using multiple hooks, you can configure the order they run in using priority, or configure one hook to be the final hook to run by using final=True. If no order is configured, the order is not guaranteed.

In the following example, the route hook will run first, then the layout hook, and the callback hook will run last.

from dash import hooks, html, Input, Output
from flask import jsonify

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

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

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

Note: There can be only one final hook across all packages that use dash.hooks in an environment. If multiple hooks use it, users will see a “dash.exceptions.HookError: Final hook already present” error when running a Dash app.

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 external JavaScript scripts to apps. hooks.script takes a list of dicts where each dict is one external script to add. The following example adds a Google Analytics URL. external_url is the location of the script to add to the app. And external_only specifies there is no local version of the script. This ensures it works with apps that have serve_locally=True, the default in Dash.

from dash import hooks

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

Adding the above using hooks.script is equivalent to the app developer adding a script with external_scripts on the dash.Dash constructor:

import dash

app = dash.Dash(external_scripts=['https://www.google-analytics.com/analytics.js'])

Adding External Stylesheets with hooks.stylesheet

Use hooks.stylesheet to add external stylesheets to apps. hooks.stylesheet takes a list of dicts where each dict is one external stylesheet to add. The following example adds Bootstrap CSS. external_url is the location of the script to add to the app. And external_only specifies there is no local version of the stylesheet. This ensures it works with apps that have serve_locally=True, the default in Dash.

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}])

Adding the above using hooks.stylesheet is equivalent to the app developer adding a stylesheet with external_stylesheets on the dash.Dash constructor:

import dash

app = dash.Dash(external_stylesheets=['https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css'])

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(priority=1)
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.