Writing Dash Plugins using Dash Hooks

New in Dash 3.0. Install the Dash 3.0 release candidate with pip install dash==3.0.0rc1

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.

Plugin Discovery

For a plugin to be available to Dash, that plugin needs to define a dash-hooks entry point. Here’s an example of a setup.py file for a plugin which defines a dash-hooks entry point:

from setuptools import setup

setup(
    name="my_dash_plugin", # Package name
    packages=["my_dash_plugin"], # The package code
    entry_points={"dash-hooks": ["my_dash_plugin = my_dash_plugin"]} # `dash-hooks` entry point for a plugin called "my_dash_plugin", which runs the my_dash_plugin package
    ...
)

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. The plugin can add to an app’s layout, callbacks, routes or callback error handlers before an app is served.

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.

Available Hooks

Dash’s hooks system allows external plugins to add functionality to Dash apps.

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 global callback error handler. The decorated function will be set as the global error handler. 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:

@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 <body> tag.

@hooks.index()
def update_index(app_index):
    body = "<body>"
    body_idx = app_index.find(body) + len(body)
    updated_app_index = app_index[body_idx:] + '<div>Added content<div>' + app_index[: body_idx + 1]
    return updated_app_index

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.

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:

hooks.clientside_callback(
    """
    function(n_clicks) {
        if (n_clicks) {
            return {"display": "none"};
        }
        return {"display": "block"};
    }
    """,
    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.

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.

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

Example: Creating a Plugin

Let’s create 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

Create a Python project with the following structure.

callback_error_plugin/
├── __init__.py
setup.py

Write the Plugin Code in __init__.py

  1. Open the __init__.py file and 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. Import hooks, set_props, Input, and Output from dash to use later.

    ```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={
    “background”: “none”,
    “border”: “none”,
    “position”: “absolute”,
    “top”: “5px”,
    “right”: “10px”,
    },
    ),
    ],
    style={
    “padding”: “15px”,
    “margin”: “10px 0”,
    “border”: “1px solid #f5c6cb”,
    “backgroundColor”: “#f8d7da”,
    “position”: “relative”,
    },
    )
    ],
    id=”callback-error-banner-wrapper”,
    )
    ]
    `` 2. Use thehooks.layoutdecorator to add the component to the app layout. Give this hook a priority of1`. 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])

  2. 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")

  3. Use the hooks.error decorator to add a global 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"There was an error: {err}"))

Configure the Dash Entrypoint in setup.py

Add the following to the setup.py file. Configuring a "dash-hooks" entry point makes the plugin available to Dash.

from setuptools import setup

setup(
    name="callback_error_plugin",
    version="0.0.1",
    install_requires=[
        "dash",
    ],
    entry_points={"dash-hooks": ["callback_error_plugin = callback_error_plugin"]},
    packages=["callback_error_plugin"],
)

Build the Plugin

To build an installable plugin, use build.

pip install build

To build the plugin, run:

python -m build

This will generate a Python wheel and source distribution 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.0.
  3. Install the wheel or source distribution created in the earlier “Build the Plugin” section with pip install <path-to-generated-dist>.
  4. Create an app that will run with the new plugin. Here’s an example with an app in app.py that will show a division by zero error when 0 is entered in the input.

    ```python
    from dash import dcc, html, callback, Input, Output, Dash

    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.py`.