Help Needed: Circular Dependency in Dash Callbacks and URL Handling

Hi everyone,

I’m working on a Dash app that visualizes Jira data fetched from a MongoDB database. The app allows users to filter data by date range, issue type, workgroup, and granularity, and then updates the URL based on the selected filters. However, I’m encountering a circular dependency issue that I haven’t been able to resolve despite multiple attempts.

The Problem

I keep getting the following error:

Error: Dependency Cycle Found: store-date-picker-range.data -> store-url-params.data -> url.search -> date-picker-range.start_date -> store-date-picker-range.data

I’ve tried different approaches to decouple the dependencies, such as using dcc.Store to hold intermediate states and directly handling URL updates via dcc.Location, but the circular dependency persists.

Current Implementation Overview

  1. Date Range Picker: Allows users to select a start and end date.
  2. Issue Type Dropdown: Lets users select multiple issue types (e.g., Feature, Bug).
  3. Workgroup Dropdown: Lets users filter by specific workgroups.
  4. Granularity Dropdowns: Control the granularity (daily, weekly, monthly) for the charts.
  5. URL Sync: Updates the URL parameters based on the selected filters and also reads from the URL to initialize the app.

Callbacks Setup

  1. Callback 1: Updates store-date-picker-range when the date picker changes.
  2. Callback 2: Updates the URL (url.search) based on the current state of all filters (including data from store-date-picker-range).
  3. Callback 3: Initializes the app state by reading parameters from url.search.
  4. Callback 4: Updates the charts based on the current state of filters (using data from store-date-picker-range).

What I’ve Tried

  • Removing dcc.Store for URL Params: Attempted to remove intermediate stores and directly manage URL handling via dcc.Location.
  • Separate Callbacks: Attempted to separate the concerns of reading from and writing to the URL.

Code Example

Here is a simplified version of the problematic part of my app:

import dash
from dash import dcc, html
from dash.dependencies import Input, Output, State
from urllib.parse import urlencode, parse_qs

app = dash.Dash(__name__)

app.layout = html.Div([
    dcc.Location(id='url', refresh=False),
    dcc.Store(id='store-date-picker-range'),
    dcc.DatePickerRange(id='date-picker-range'),
    dcc.Dropdown(id='issue-types', options=[...], multi=True),
    dcc.Dropdown(id='workgroup-dropdown', options=[...], multi=True),
    dcc.Dropdown(id='velocity-granularity', options=[...]),
    dcc.Dropdown(id='burnup-granularity', options=[...]),
    dcc.Dropdown(id='status-changes-granularity', options=[...]),
    dcc.Graph(id='velocity-chart'),
    dcc.Graph(id='burnup-chart'),
    dcc.Graph(id='status-changes-line-chart'),
])

@app.callback(
    Output('store-date-picker-range', 'data'),
    [Input('date-picker-range', 'start_date'),
     Input('date-picker-range', 'end_date')]
)
def update_date_store(start_date, end_date):
    return {'start_date': start_date, 'end_date': end_date}

@app.callback(
    Output('url', 'search'),
    [Input('velocity-granularity', 'value'),
     Input('burnup-granularity', 'value'),
     Input('status-changes-granularity', 'value'),
     Input('workgroup-dropdown', 'value'),
     Input('store-date-picker-range', 'data'),
     Input('issue-types', 'value')]
)
def update_url(velocity_granularity, burnup_granularity, status_changes_granularity, selected_workgroups, date_picker_range, issue_types):
    params = {
        'start_date': date_picker_range['start_date'],
        'end_date': date_picker_range['end_date'],
        'issue_types': issue_types,
        'velocity_granularity': velocity_granularity,
        'burnup_granularity': burnup_granularity,
        'status_changes_granularity': status_changes_granularity,
        'selected_workgroups': selected_workgroups
    }
    return '?' + urlencode(params, doseq=True)

@app.callback(
    [Output('date-picker-range', 'start_date'), Output('date-picker-range', 'end_date'),
     Output('issue-types', 'value'), Output('velocity-granularity', 'value'),
     Output('burnup-granularity', 'value'), Output('status-changes-granularity', 'value'),
     Output('workgroup-dropdown', 'value')],
    [Input('url', 'search')]
)
def load_config_from_url(search):
    if not search:
        raise dash.exceptions.PreventUpdate

    params = parse_qs(search.lstrip('?'))
    return (
        params.get('start_date', [None])[0],
        params.get('end_date', [None])[0],
        params.get('issue_types', [''])[0],
        params.get('velocity_granularity', ['W'])[0],
        params.get('burnup_granularity', ['D'])[0],
        params.get('status_changes_granularity', ['D'])[0],
        params.get('selected_workgroups', [[]])[0] or []
    )

@app.callback(
    [Output('velocity-chart', 'figure'), Output('burnup-chart', 'figure'),
     Output('status-changes-line-chart', 'figure')],
    [Input('store-date-picker-range', 'data'),
     Input('issue-types', 'value'), Input('velocity-granularity', 'value'),
     Input('burnup-granularity', 'value'), Input('status-changes-granularity', 'value'),
     Input('workgroup-dropdown', 'value')]
)
def update_charts(date_picker_range, issue_types, velocity_granularity, burnup_granularity, status_changes_granularity, selected_workgroups):
    # Logic to update charts based on filters
    pass

if __name__ == '__main__':
    app.run_server(debug=True)

What I Need Help With

  • Breaking the Circular Dependency: How can I restructure my callbacks or state management to avoid the circular dependency issue?
  • Best Practices: Are there best practices for handling URL synchronization with state in Dash to prevent such issues?

Any help or suggestions would be greatly appreciated!

Thank you in advance for your support!

I’ve not, with small changes, managed to get your example to reproduce that error, but I think (apologies if I’ve misunderstood something) this is broadly how it is:

First an observation - store-data-picker-range may not be needed if it just duplicates properties of data-picker-range

If we drop that, I think the core challenge is that there are properties you’re trying to keep in sync with each other e.g. (‘url’,‘search’) and (‘date-picker-range’,‘start-date’/‘end-date’). Either can be changed by user action, and that change has to be reflected in the other element.

The Dash rules are that you can’t have circular dependencies, except that callbacks are allowed to have the same element property as both an input and an output. So this means you have to have a single callback that takes all the synchronised properties as both inputs and outputs.

Something like:

@app.callback(
    Output('url', 'search'),
    Output('date-picker-range', 'start_date'),
    Output('date-picker-range', 'end_date'),
    Input('url', 'search'),
    Input('date-picker-range', 'start_date'),
    Input('date-picker-range', 'end_date'),
    prevent_initial_call=False
)
def update_date_store(url, start_date, end_date):
    trigger = ctx.triggered_id
    if trigger = 'url':
        ...    # update date-picker-range
    else:
        ...    # update url

This won’t be pretty if you want to keep the url synchronised with a lot of other elements. But I don’t really understand why there’s a need to do that, and I don’t think many web apps do it. There are better ways of providing persistence, if that’s the aim.

Thanks for your suggestion! I tried it, and while it eliminated the circular dependency, it still doesn’t work quite as I intended. I’m considering rolling back to the last working version and exploring alternative approaches that don’t involve syncing URLs with multiple elements.

My main goal is to make it easy for users to view data for specific teams (each team has a set of workgroups). Initially, I thought using URLs to persist selections would be a good solution so that users could share or bookmark them.

However, you mentioned there are better ways to achieve persistence. Could you elaborate on these methods or point me in the right direction?

One idea I had was to define which workgroups should be filtered out for each team and then use that information in the URL. Do you think that could be a viable approach?

I didn’t think the code through enough - trigger is None should be treated the same as a url change, not as a date-picker change, I think. There could be other issues, but I have had code like this working in other apps

And I can see why you may want users to be able to copy and post URLs that reflect the state of the app.

For persistence, most components (in dcc, dbc and dmc I think) have properties persistence, persisted_props and persistence_type that control how they retain properties on refresh or closing a browser tab. For dcc.Store() the relevant property is storage_type

Avoiding Circular Dependencies in Dash When Syncing Dropdowns Across Tabs

Hello everyone,

I’m building a multi-tab Dash dashboard where several dcc.Dropdown components appear across different tabs. Each dropdown represents the same entity (e.g., a “NIT” identifier), but depending on which tab you’re on, the layout may have a different ID for it (e.g., 'nit-dropdown', 'nit-emisores-dropdown', etc.).

What I’m Trying to Do

I want all these dropdowns to stay synchronized. That is:

  • When a user selects a value in any one of the dropdowns, the same value should automatically populate in the others.
  • To handle this, I store the selected value in a hidden dcc.Store ('nit-store'), and then use that to update all dropdowns.

The logic works fine functionally. However…


The Problem: Circular Dependencies

Dash gives me the following error:

rust

CopiarEditar

Error: Dependency Cycle Found: nit-dropdown.value -> nit-store.data -> nit-dropdown.value

This makes sense because:

  1. Dropdown A updates nit-store.data via a callback.
  2. Then, another callback reads from nit-store.data to update all dropdowns (including A).
  3. This creates a loop:
    A.value → store.data → A.value

Even if the update to the same dropdown is ignored inside the callback (e.g., using PreventUpdate), Dash still considers this a dependency cycle at the graph level, which hurts performance and stability.


What I Tried

  • I attempted using pattern-matching callbacks (ALL, MATCH) to target only non-triggering dropdowns.
  • Tried manually filtering updates based on the callback_context.
  • Even returned early if the dropdown’s value already matches the one in the store.

However, Dash still sees the circular chain of dependencies and throws a warning or slows down the dashboard.


What I’m Looking For

  • A clean way to synchronize dropdowns without introducing circular dependencies.
  • Ideally, a solution where only the other dropdowns (not the one that triggered the change) are updated.
  • Or, a workaround that Dash accepts without triggering circular dependency warnings.

Any help or patterns that have worked for you in similar cases would be much appreciated!

heres is the code I am using rigth now:

@app.callback(
    Output({'type': 'nit-dropdown', 'index': ALL}, 'value'),
    Input('nit-store', 'data'),
    State({'type': 'nit-dropdown', 'index': ALL}, 'id'),
    prevent_initial_call=False
)
def sync_dropdown(store_data, dropdown_ids):
    if not store_data or 'triggered' not in store_data:
        raise PreventUpdate

    triggered_index = store_data['triggered']
    new_value = store_data['value']

    updated_values = []
    for dropdown_id in dropdown_ids:
        if dropdown_id['index'] == triggered_index:
            # Do not update the dropdown that triggered the store change
            raise PreventUpdate
        else:
            # Update the others
            updated_values.append(new_value)

    return updated_values

Hi @Blackglass35 and welcome to the Dash community :slightly_smiling_face:

Are all the nit dropdowns the same in all the tabs? If so, you could just use the same component with the same id in each tab. You could also try setting persistence=True - then you would not have to use the dcc.Store to synchronize them.

If that’s not what you are looking for, then if you include a complete minimal example then it will be easier to help.

wouldn’t that return a duplicated id error? the dropdowns are the same in al the tabs tho

tried this:

def call_contacta(app, caratulas, pagadores, dfRf):
    @app.callback(
    Output('contacto-card', 'children'),
    Input('nit-dropdown', 'value'),
    prevent_initial_call=False, 
    allow_duplicate=True
    )... some more code

this is for every call_back I have.
the for every layout i did:

from complementos.dropdown import dropdown_nit
def layout_contacta(caratulas, nits):
    return dcc.Tab(label='Contacta', className='tab-custom', selected_className='tab-selected',children=[
            dbc.Col([
                html.P("..."),
                html.Label("...: "),
                dropdown_nit(nits),
            ]),

It did solve the circular dependencies problem, but now the callbacks are not responding to the value choosed in the dropdown, and the dropdowns in other tabs are not getting updated with the value choosed

@Blackglass35

I can’t tell what’s wrong based on the code you posted.

Here’s an example of what I mean - try running this example locally. You will see the dropdowns sync’s between the two tabs and the callback in each tab is based on the dropdown value.



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

app = Dash(suppress_callback_exceptions=True)

app.layout = html.Div([
    html.H1('Dash Tabs component demo'),
    dcc.Tabs(id="tabs-example", value='tab-1-example', children=[
        dcc.Tab(label='Tab One', value='tab-1-example'),
        dcc.Tab(label='Tab Two', value='tab-2-example'),
    ]),
    html.Div(id='tabs-content-example')
])


@callback(
    Output('tabs-content-example', 'children'),
    Input('tabs-example', 'value')
)
def render_content(tab):
    if tab == 'tab-1-example':
        return html.Div([
            html.H3('Tab content 1'),
            dcc.Dropdown(options=["A", "B", "C"],  id="nit", persistence=True),
            html.Div(id="tab-1-selection")

        ])
    elif tab == 'tab-2-example':
        return html.Div([
            html.H3('Tab content 2'),
            dcc.Dropdown(options=["A", "B", "C"], id="nit", persistence=True),
            html.Div(id="tab-2-selection")
        ])

@callback(
    Output("tab-1-selection", "children"),
    Input("nit", "value")
)
def update_tab_1(v):
    return f"Tab 1 selection: {v}"


@callback(
    Output("tab-2-selection", "children"),
    Input("nit", "value")
)
def update_tab_2(v):
    return f"Tab 2 selection: {v}"


if __name__ == "__main__":
    app.run(debug=True)


Hey,

I have similar situation like @HarriT , My App has several pages with multiple filter options like dropdowns, searchable inputs and AG Grid, which columns can be filtered as well + columns can change positions. I want an option for the user to custom filter the data on the page, custom filter AG Grid and change columns position and share her/his filter settings with other users. So far my custom solution is also running in circular dependencies. How would you address such situation?

Thanks Ivan

Hey @HarriT ,

my initial problem was timing and it looks for me like you are experiencing the same issue.

I managed to get rid of race condition between URL update callback and pramas update calbacks by using prevent_initial_call and more sophisticated callback chain that ensures proper ordering. In my callbacks I am measuring components state (like dropdowns, date range, etc.) and URL State.

Added to pages:

dcc.Location(id=f'{page_id}-url', refresh=False),
dcc.Store(id=f'{page_id}-component-state', data=None),
dcc.Store(id=f'{page_id}-url-state', data=None),

and then trough chain of callbacks measuring the most recent change

# Split the bidirectional sync into unidirectional flows
# This prevents the circular dependency

# Step 1: Component changes → Store
@callback(
    Output(f'{page_id}-component-state', 'data'),
    [Input(grid_id, 'filterModel'),
     Input(grid_id, 'columnState'),
     Input('indicator-selector', 'value')],
    prevent_initial_call=True  # Only trigger on actual changes
)
def capture_component_state(filters, columns, indicator):
    """Capture component state changes"""
    return {
        'filters': filters,
        'columns': columns,
        'indicator': indicator,
        'timestamp': time.time(),
        'source': 'component'
    }

# Step 2: Store → URL (only if from component)
@callback(
    Output(f'{page_id}-url', 'href'),
    Input(f'{page_id}-component-state', 'data'),
    [State(f'{page_id}-url', 'href'),
     State(f'{page_id}-url-state', 'data')],
    prevent_initial_call=True
)
def update_url_from_store(component_state, current_href, url_state):
    """Update URL only from component changes"""
    if not component_state or component_state.get('source') != 'component':
        raise PreventUpdate
    
    # Check if URL state is more recent (race condition check)
    if url_state and url_state.get('timestamp', 0) > component_state.get('timestamp', 0):
        raise PreventUpdate
    
    base_url = current_href.split('?')[0]
    return build_url_with_all_params(base_url, **component_state)

# Step 3: URL → Store (only on initial load or back/forward)
@callback(
    Output(f'{page_id}-url-state', 'data'),
    Input(f'{page_id}-url', 'search'),
    prevent_initial_call=False  # DO trigger on initial load
)
def capture_url_state(search):
    """Parse URL state"""
    params = parse_url_params(search)
    return {
        'filters': parse_json_param(params.get('filters', '{}')),
        'columns': parse_json_param(params.get('columns', '[]')),
        'indicator': params.get('indicator'),
        'timestamp': time.time(),
        'source': 'url'
    }

# Step 4: Apply URL state to components (only if from URL)
@callback(
    [Output(grid_id, 'filterModel'),
     Output(grid_id, 'columnState'),
     Output('indicator-selector', 'value')],
    Input(f'{page_id}-url-state', 'data'),
    [State(f'{page_id}-component-state', 'data')],
    prevent_initial_call=False  # DO trigger on initial load
)
def apply_url_state_to_components(url_state, component_state):
    """Apply URL state to components"""
    if not url_state:
        raise PreventUpdate
    
    # Don't apply if component state is more recent
    if component_state and component_state.get('timestamp', 0) > url_state.get('timestamp', 0):
        raise PreventUpdate
    
    return (
        url_state.get('filters', {}),
        url_state.get('columns', []),
        url_state.get('indicator')
    )

I think this approach may work for you too.