I’m working with multi-page apps and trying to understand how to cancel background callbacks on page refresh or page exit.
I created a simple Dash multi-page app to demonstrate the issue. Below is the project directory and code:
- app.py - sets up a multi-page app with support for background callbacks that uses cache. Run locally.
- pg0_home.py - the base path for the multi-page app. Shows a link to the other accessible page in this multi-page app.
- pg1_number_counter.py - a simple application that uses a background callback to update a counter based on button click.
C:.
│ app.py
│
└───pages
│ pg0_home.py
│ pg1_number_counter.py
│
├───cache
│ cache.db
│
└───__pycache__
pg0_home.cpython-38.pyc
pg1_number_counter.cpython-38.pyc
app.py
import dash
from dash import Dash, html, DiskcacheManager
import dash_bootstrap_components as dbc
import diskcache
import os
# support for background callbacks
cache_path = os.path.join(os.getcwd(), 'pages', 'cache')
cache = diskcache.Cache(cache_path)
background_callback_manager = DiskcacheManager(cache)
app = Dash(__name__,
external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.themes.DARKLY, dbc.icons.BOOTSTRAP],
use_pages=True, suppress_callback_exceptions=True,
background_callback_manager=background_callback_manager)
app.layout = html.Div([
dash.page_container
])
if __name__ == '__main__':
app.run(debug=True)
pg0_home.py
import dash
from dash import html
import dash_bootstrap_components as dbc
dash.register_page(__name__,
path = '/',
title = 'Home',
name = 'Home',
description='Home')
layout = html.Div([
dbc.Container([
dbc.Row([
html.H1("Welcome!", className="text-center"),
], style={'margin-top': '250px'}),
dbc.Row([
html.P(children=[html.A("Number counter", href="/number-counter")], className="text-center")
])
])
])
pg1_number_counter.py
import dash
from dash import html, dcc, Input, Output, State, callback
from dash.exceptions import PreventUpdate
import dash_bootstrap_components as dbc
import time
dash.register_page(__name__,
path = '/number-counter',
title = 'Number Counter',
name = 'Number Counter',
description='Simple app demonstration')
layout = html.Div([
dbc.Container([
dbc.Row([
dbc.Col(dcc.Link(html.H3("", className="bi bi-list"), href="/"), width={"size": 2}, className="mt-3")
]),
dbc.Row([
html.H3("0", id="current-count", className="text-center"),
], style={'margin-top': '250px'}),
dbc.Row([
dbc.Col([
dbc.Button("Stop", id="trigger-btn", color="danger"),
], width="auto"),
], justify="center"),
dbc.Row([
dbc.Col([
dcc.Location(id="pg-url", refresh=False),
])
])
])
])
@callback(
Output('trigger-btn', 'children'),
Output('trigger-btn', 'color'),
Input('trigger-btn', 'n_clicks'),
State('trigger-btn', 'children'),
)
def on_click_change_styling(_, curr_btn_text):
"""
Update the button styling each time the user clicks the button
"""
if curr_btn_text == 'Start':
return 'Stop', 'danger'
elif curr_btn_text == 'Stop':
return 'Start', 'primary'
else:
raise PreventUpdate
@callback(
Output('current-count', 'children'),
Input('trigger-btn', 'children'),
State('trigger-btn', 'color'),
State('current-count', 'children'),
background=True,
cancel=[Input('trigger-btn', 'n_clicks'), Input("pg-url", "pathname"),],
progress=[Output('current-count', 'children')],
prevent_initial_call=True,
)
def update_progress(set_progress, btn_text, btn_color, curr_count):
"""
If the button says 'Stop', periodically update the count until the user clicks the button again
"""
# calculations should only be done if btn has appropriate values
if btn_text != 'Stop' or btn_color != 'danger':
raise PreventUpdate
row_counter = int(curr_count)
while(True):
# increase the count every second
time.sleep(1)
# update the row counter
row_counter+=1
print(f"row_counter = {row_counter}")
# Update the user on the current progress of the counts
set_progress(str(row_counter))
I have successfully set-up the logic of pg1_number_counter.py to cancel on 2 situations:
- The user explicitly cancels the background callback by updating the property listed in cancel: here, it is clicking the button.
- The user navigates to a different page in the multi-page app: since there are only 2 paths, this happens navigating from /number-counter to /
However, there are 2 situations where I want to cancel the background callback but I can’t
- The user refreshes on path /number-counter
- The user exits on path /number-counter
My current idea is this: I was able to create a simple html page that triggers a pop-up on page exit or page refresh.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blank Page</title>
<script>
window.onbeforeunload = function() {
return "";
};
</script>
</head>
<body>
</body>
</html>
If I could create a clientside callback with this pop-up logic, it would also include logic to update the button click if the btn is red and the user clicked ‘Reload’ or ‘Leave’ (I’ll just call it ‘confirm’). Below is rough pseudo-code (obviously doesn’t work)
clientside_callback(
"""
function(pop_up, curr_btn_clicks, btn_color) {
// html code is put somewhere here
while(pop_up != None) {
// only increase the btn click count if the background callback is still in progress
if (window.pop_up_confirm == True && btn_color == 'danger') {
return curr_btn_clicks + 1;
}
}
}
""",
Output('trigger-btn', 'n_clicks'),
Input('window', 'pop_up'),
State('trigger-btn', 'n_clicks'),
State('trigger-btn', 'color'),
prevent_initial_call=True,
)
I’ve only ever used simple clientside callbacks that print to the console, so wondering if I’m going in the right direction? I see some examples of clientside callbacks here (Clientside Callbacks | Dash for Python Documentation | Plotly), but not addressing my specific case. Or is there a different approach altogether to cancel a background callback on page refresh or exit? Thanks!