Skip to content

[4.2] Persistent callbacks#3771

Open
T4rk1n wants to merge 5 commits into
v4.2from
persistent-callbacks
Open

[4.2] Persistent callbacks#3771
T4rk1n wants to merge 5 commits into
v4.2from
persistent-callbacks

Conversation

@T4rk1n
Copy link
Copy Markdown
Contributor

@T4rk1n T4rk1n commented May 8, 2026

This PR adds support for:

  1. Persistent callbacks - Callbacks with persistent=True don't show "Updating..." in the browser title while running
  2. No-output callbacks - Callbacks without outputs that fire on initial load, fixes [BUG] no output callbacks doesn't trigger as initial callback. #3770
  3. No-input callbacks - Callbacks without inputs (only State) that fire on initial load, resolves [Feature Request] allow for callbacks without Inputs #3411
  4. No-input no-output callbacks - Callbacks with neither inputs nor outputs that fire once on initial load

Examples

Persistent callback without inputs/outputs

import asyncio                                                                                                                                                                                                 
from dash import Dash, html, callback, set_props                                                                                                                                                               
                                                                                                                                                                                                               
app = Dash(backend="fastapi", websocket_callbacks=True)                                                                                                                                                        
                                                                                                                                                                                                               
app.layout = html.Div([                                                                                                                                                                                        
    html.Div(id="counter", children="0"),                                                                                                                                                                      
])                                                                                                                                                                                                             
                                                                                                                                                                                                               
@callback(persistent=True)                                                                                                                                                                                     
async def continuous_updates():                                                                                                                                                                                
    # Fires once on load, runs forever without showing "Updating..." title                                                                                                                                     
    n = 0                                                                                                                                                                                                      
    while True:                                                                                                                                                                                                
        set_props("counter", {"children": str(n)})                                                                                                                                                             
        n += 1                                                                                                                                                                                                 
        await asyncio.sleep(1)                                                                                                                                                                                 
                                                                                                                                                                                                               
if __name__ == "__main__":                                                                                                                                                                                     
    app.run() 

No-input callback (fires on initial load)

from dash import Dash, html, Output, State, callback                                                                                                                                                           
                                                                                                                                                                                                               
app = Dash()                                                                                                                                                                                                   
                                                                                                                                                                                                               
app.layout = html.Div([                                                                                                                                                                                        
    dcc.Store(id="config", data={"theme": "dark"}),                                                                                                                                                            
    html.Div(id="output"),                                                                                                                                                                                     
])                                                                                                                                                                                                             
                                                                                                                                                                                                               
@callback(                                                                                                                                                                                                     
    Output("output", "children"),                                                                                                                                                                              
    State("config", "data"),                                                                                                                                                                                   
)                                                                                                                                                                                                              
def initialize_from_config(config):                                                                                                                                                                            
    # Fires once on page load with State values                                                                                                                                                                
    return f"Theme: {config['theme']}"                                                                                                                                                                         
                                                                                                                                                                                                               
if __name__ == "__main__":                                                                                                                                                                                     
    app.run() 

No-input no-output callback (side-effect only)

from dash import Dash, html, callback                                                                                                                                                                          
                                                                                                                                                                                                                 
app = Dash()                                                                                                                                                                                                   
app.layout = html.Div([html.Div(id="output")])                                                                                                                                                                 
                                                                                                                                                                                                               
@callback()                                                                                                                                                                                                    
def log_page_load():                                                                                                                                                                                           
    # Fires once when page loads                                                                                                                                                                               
    print("Page loaded!")                                                                                                                                                                                      
    # Could log to database, initialize connections, etc.                                                                                                                                                      
                                                                                                                                                                                                               
if __name__ == "__main__":                                                                                                                                                                                     
    app.run() 

@T4rk1n T4rk1n changed the title ] Persistent callbacks [4.2] Persistent callbacks May 8, 2026
Copy link
Copy Markdown
Contributor

@camdecoster camdecoster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a few comments, but this is looking good.

Comment on lines +113 to +130
const noOutputCallbacks = (graphs.callbacks || [])
.filter(cb => cb.noOutput && !cb.prevent_initial_call)
.map(cb => {
const resolved = makeResolvedCallback(cb, resolveDeps(), '');
resolved.initialCall = true;
return resolved;
})
.filter(cb => {
// If no inputs, always include (fires once on initial load)
if (cb.callback.inputs.length === 0) {
return true;
}
// Check if any input is in the layout
const inputs = cb.getInputs(paths);
return inputs.some(inp =>
Array.isArray(inp) ? inp.length > 0 : inp
);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You iterate through the callbacks three times. What do you think of switching to a for...of loop that does everything in one pass? In fact, you could use one loop that handles both noOutputCallbacks and noInputCallbacks.

Comment thread dash/_utils.py
Comment on lines +180 to +181
# Fallback to empty hash if no external frame found
return _hash_inputs()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Fallback to empty hash if no external frame found
return _hash_inputs()

Comment thread dash/_utils.py
# Get the call site of the @callback decorator
stack = inspect.stack()
# Walk up the stack to find the actual callback call site
# (skip internal dash package frames)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# (skip internal dash package frames)
# Fallback to empty hash if no external frame found
# (skip internal dash package frames)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants