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