Connecting dcc.link() and dcc.store() capabilities

rewrote the intro and title based on the feedback from @jinnyzor

I have a multi-page app. On page1, imagine A, B, C, D links each with dcc.Link(letter, href=f’/page2?opt={letter}'). The option is essentially an input parameter. That gets me to page2 and I am able to mangle the option out of the parameters passed as Input(‘url’, ‘search’). But the “?opt=A” parameter is still in the url and will remain there when other options are selected once the user is on that page.

Is there any way to connect a dcc.Store to a dcc.Link call so the new page can get the desired setting without passing URL parameters. I don’t want to use a button for any page changes. Or are there any other tricks to do this?

Hello @marketemp,

Are you using index and routing via a callback or using the pages of Dash?

You could potentially store the rest of the parameters inside of a dcc.Store with the callback. What are you using the parameters for?

1 Like

My app is a stock app and I am on page market that displays market info. The user would click a link to go to the a stock page with info on the selected stock, currently passed as a stock=AAPL url parameter, but I want that cleaned up.

dcc.Store looks pretty good, but would I then have to build the mechanism to change pages and set the value in the store with a Button instead of a link?

Without seeing your code, I don’t really know. But I would think so, and you’d have to query it in the responses as well.

1 Like

You don’t need to see my code. I’m not doing it right and why I’m asking. I need some way where clicking a dcc.Link (preferably not a button) also sets a dcc.Store value as you’ve showed me to get the parameter-like setting invoked on the next page without a passing URL parameter.

The parameters are associated with the link, hardcoded or is there some sort of input that the user would populate?

If it is hardcoded, then your dcc.store could be a dictionary of your routings.

If it is based upon some user interaction, then you just need to output that interaction to the dcc.store for the parameters.

1 Like

OK, here is code. I am on a portfolio page where the user has selected X stocks. That selection at the moment calls a routine that makes the following table that is passed into a Div Output (showing only the first Stock column and ignoring the price data that’s also shown in additional columns:

html.Table([
    html.Tr([html.Td(dcc.Link('META', href=f'/stock?sym='META'))]),
    html.Tr([html.Td(dcc.Link('AAPL', href=f'/stock?sym='AAPL'))]),
    html.Tr([html.Td(dcc.Link('AMZN', href=f'/stock?sym='AMZN'))]),
    html.Tr([html.Td(dcc.Link('NFLX', href=f'/stockt?sym='NFLX'))]),
    html.Tr([html.Td(dcc.Link('GOOG', href=f'/stock?sym='GOOG'))]),
    html.Tr([html.Td(dcc.Link('TSLA', href=f'/stock?sym='TSLA'))]),
])

Right now I can click the link, get to the stock page, mangle the stock symbol out of the URL. But it remains in the URL without a second double-refresh pass using dcc.Location.

What I need is a way for the user to click the link and seamlessly set a dcc.Store value, or any other mechanism to communicate what stock was selected and shown on the stock page that only focuses on one selected stock.

Of course this would be easy with a dropdown menu and button, but I want this to be an ordinary link from within the table and have the stock setting be invisible as the page transfer occurs. The perfect solution would be a dcc.Store parameter that somehow set the desired value(s) that are normally passed via URL parameters within href or other Inputs as far as Dash operates to get one-click + selection-is-hidden operation.

@marketemp,

Thank you for the info on what you are trying to accomplish. Here is what I came up with:

from dash import dcc, html, no_update, Dash, Output, Input, State, page_container, ALL, ctx
import dash

app = Dash(__name__, use_pages=True, pages_folder='')

app.layout = html.Div(
    [
        dcc.Link('Home', href='/'),
        html.Br(),
        html.Br(),
        dcc.Store(id='selectedStock'),
        page_container]
)

stockList = ['META', 'AAPL', 'AMZN', 'NFLX', 'GOOG', 'TSLA']

def stocks():
    layout = html.Table([
        html.Tbody([
        html.Tr([html.Td(dcc.Link(i, href='/stocks'), id={'index':i, 'type':'stock'}, n_clicks=0)]) for i in stockList
        ])
    ])
    return layout

app.clientside_callback(
    """
        function (i) {
            if (i.reduce((a, b) => a + b, 0) > 0) {
                stock = JSON.parse(dash_clientside.callback_context.triggered[0].prop_id.split('.n_clicks')[0]).index
                return stock
            }
            return ''
        }
    """,
    Output('selectedStock', 'data'),
    Input({'index':ALL, 'type':'stock'}, 'n_clicks'),
    Prevent_Inital_Call=True
)

dash.register_page('Home', path='/', layout=stocks())

dash.register_page('Stock Details', path='/stocks', layout=html.Div([html.Div(id='hiddenTrigger'),
                                                                     html.Div(id='stockInfo')]))

@app.callback(
    Output('stockInfo', 'children'),
    Input('hiddenTrigger','n_clicks'),
    State('selectedStock','data')
)
def caterStock(v, s):
    return s

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

It places a pattern matching index in the td cell, which can have the n_clicks property. When clicked, it updates the selectedStocks dcc.Store, this is clientside because of the speed needed to make it accurate. When the page stocks is navigated to, it is empty, but the div has an n_clicks property, which on rendering triggers a callback. This callback queries the dcc.Store and responds with the result.

@AnnMarieW, it’s a clientside pattern matching callback. :stuck_out_tongue:


Edit:

For those interested, i.reduce((a, b) => a + b, 0) adds all the n_clicks together to make sure that it doesnt trigger when the page is navigated back to, in that instance, the sum would be 0.

1 Like

Your program seems to be exactly what I need. I’ll let you know when I get it implemented.
I still haven’t transitioned to the new multi-page app structure but doubt that will matter.

Thanks @jinnyzor !!!

2 Likes

@jinnyzor confirming this has no dependency on the new Dash paging mechanism. I’m not sure whether to put one in before the other. I assume it doesn’t, but don’t want to get frustrated on a failed attempt in case it does… My app uses dbc.NavItem

@marketemp,

Correct, this doesn’t depend upon it. But the pages registry does make it harder to implement.

You should be able to use something similar, just when you show the page, you put the trigger there. Or you could even include the dcc.Store as a state in the callback, but I’m not sure how well that will work.

1 Like

@jinnyzor

I took your work one step further to get what I needed. I need the link response to be loaded as the default value of a dropdown containing all of the symbols. Here is what I got to work

from dash import dcc, html, no_update, Dash, Output, Input, State, page_container, ALL, ctx
import dash
import dash_bootstrap_components as dbc
from dash_bootstrap_templates import load_figure_template

app = Dash(__name__, use_pages=True,
           external_stylesheets=[dbc.themes.LITERA])
load_figure_template("bootstrap")


app.layout = html.Div(
    [
        dcc.Link('Home', href='/'),
        html.Br(),
        html.Br(),
        dcc.Store(id='storeStock'),
        page_container]
)

stockList = ['META', 'AAPL', 'AMZN', 'NFLX', 'GOOG', 'TSLA']


def layout_home():
    layout = html.Table([
        html.Tbody([
            html.Tr([html.Td(
                # dcc.Link(s, href='/stocks'),
                dbc.Button(s, href='/stocks', color='dark', className='text-white'),
                id={'index': s, 'type': 'stock'}, n_clicks=0)]) for s in stockList])
    ])
    return layout


def layout_stocks():
    layout = html.Div([
        html.Div('Stocks'),
        html.Div(dcc.Dropdown(id=f'dds_stock',
                              options=stockList,
                              persistence=True,
                              persistence_type='session',
                              style={'width': '100px'})),
        html.Div(id='hidden_stock_trigger'),
        html.Div(id='selected_stock')
    ])
    return layout


app.clientside_callback(
    """
        function (i) {
            if (i.reduce((a, b) => a + b, 0) > 0) {
                stock = JSON.parse(dash_clientside.callback_context.triggered[0].prop_id.split('.n_clicks')[0]).index
                return stock
            }
            return ''
        }
    """,
    Output('storeStock', 'data'),
    Input({'index': ALL, 'type': 'stock'}, 'n_clicks'),
    Prevent_Inital_Call=True
)

dash.register_page('Home', path='/', layout=layout_home())

dash.register_page('Stock Details', path='/stocks', layout=layout_stocks())


@app.callback(
    Output('dds_stock', 'value'),
    Input('hidden_stock_trigger', 'n_clicks'),
    State('storeStock', 'data')
)
def cater_stock(v, s):
    return s


@app.callback(
    Output('selected_stock', 'children'),
    Input('dds_stock', 'value'),
)
def select_stock(stock):
    return stock


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

I got it working with links and buttons.
I am curious why it won’t allow me to rename the storeStock callback to store_stock and still work?

And with a few hours of effort I have it working in my code. It is working with links and buttons across six pages, two for stocks, two for etfs, and two for indexes. This might be a tough one to answer without seeing all of my code (over 22,000 lines) using the code below

app.clientside_callback(
    """
        function (i) {
            if (i.reduce((a, b) => a + b, 0) > 0) {
                stock = JSON.parse(dash_clientside.callback_context.triggered[0].prop_id.split('.n_clicks')[0]).index
                return stock
            }
            return ''
        }
    """,
    Output('storeStock', 'data'),
    Input({'index': ALL, 'type': 'stock'}, 'n_clicks'),
    Prevent_Inital_Call=True
)


@app.callback(
    Output('dds_sym-stocktemp', 'value'),
    Input('hidden_stocktemp_trigger', 'n_clicks'),
    State('storeStock', 'data')
)
def stored_stocktemp_sym(v, sym):
    return sym


@app.callback(
    Output('dds_sym-stocktemp_history', 'value'),
    Input('hidden_stocktemp_history_trigger', 'n_clicks'),
    State('storeStock', 'data')
)
def stored_stocktemp_history_sym(v, sym):
    return sym


@app.callback(
    Output('dds_sym-etftemp', 'value'),
    Input('hidden_etftemp_trigger', 'n_clicks'),
    State('storeStock', 'data')
)
def stored_etftemp_sym(v, sym):
    return sym


@app.callback(
    Output('dds_sym-etftemp_history', 'value'),
    Input('hidden_etftemp_history_trigger', 'n_clicks'),
    State('storeStock', 'data')
)
def stored_etftemp_history_sym(v, sym):
    return sym


@app.callback(
    Output('dds_sym-indextemp', 'value'),
    Input('hidden_indextemp_trigger', 'n_clicks'),
    State('storeStock', 'data')
)
def stored_indextemp_sym(v, sym):
    return sym


@app.callback(
    Output('dds_sym-indextemp_history', 'value'),
    Input('hidden_indextemp_history_trigger', 'n_clicks'),
    State('storeStock', 'data')
)
def stored_indextemp_history_sym(v, sym):
    return sym

Everything works great, except I no longer have persistence. If I change pages via a link it’s great, but whenever I change pages via the standard menu, the sym dropdowns start clear instead of having their prior values. I used to have persistence working properly setting

                           persistence=True,
                           persistence_type='session',

but the dcc.Store and callback forcefully clears the persistence. Any ideas to resolve that? I will try using multiple app.clientside_callback( calls with different type settings.

Hello @marketemp,

What is your dc.Store type?

The default is memory. You can change it to session and that might help.

1 Like

I defined it as

dcc.Store(id='storeStock', storage_type='session'),

and it’s still clearing with every page change

I got system links working in a seventh place. I tried to get one other type of segment oriented linking working using the following code and all of the other hooks appear to be in the right place, and it’s not working.

app.layout = html.Div([
    dcc.Store(id='storeStock', storage_type='session'),
    dcc.Store(id='storeSegment', storage_type='session'),
    ...

app.clientside_callback(
    """
        function (i) {
            if (i.reduce((a, b) => a + b, 0) > 0) {
                segment = JSON.parse(dash_clientside.callback_context.triggered[0].prop_id.split('.n_clicks')[0]).index
                return segment
            }
            return ''
        }
    """,
    Output('storeSegment', 'data'),
    Input({'index': ALL, 'type': 'segment'}, 'n_clicks'),
    Prevent_Inital_Call=True
)

@app.callback(
    Output('dds_hseg-segmenttemp', 'value'),
    Input('hidden_segmenttemp_trigger', 'n_clicks'),
    State('storeSegment', 'data')
)
def stored_segmenttemp_seg(v, seg):
    print(f'stored_segmenttemp_seg({seg})')
    return seg

# and links set using
                html.Div(dcc.Link(html.B(self.typ.asegtitle[seg]), href=f'/segmenttemp'),
                         id={'index': seg, 'type': 'segment'}, n_clicks=0)

and nothing seems to be firing, as indicated by the print call not firing on top of the link not triggering. Is there anything I’m missing or some conflict with a second instance of that app.clientside_callback?

Do you have anything where it would clear out the selections based upon interaction with the stock?

Also, it looks like you’re hidden for the segment temp has an extra t in it.

Are you running your app in debug mode?

1 Like

Caught the three t’s. great eye. I was going late.

With that fix it now calls stored_segmenttemp_seg returning seg = ‘’ on initiation but never again with any clicks on the links. Instead of stock symbols, the values I am looking for with segment are string values of integers [101-121 and 200]. The default is ‘101’ and if I get stored_segmenttemp_seg to return ‘102’ or another number that does pass the right value to change the page.

Changing d={'index': seg to d={'index': str(seg) didn’t do the trick either on the link side.

I wish I understood JavaScript and Dash well enough to debug the app.clientside_callback line and I’m still going to get learning…

There are no other giveaways running it in debug mode. I am getting a different error of

A nonexistent object was used in an `Input` of a Dash callback. The id of this object is `graph_syms_bell-segmenttemp` and the property is `hoverData`. The string ids in the current layout are: [url, session_id, ...]

which is weird because I am definitely getting a nice dict of hoverData info sent which I used to dynamically load two lower graphs based on the symbol that’s hovered in the top graph

you can see how hovering over AAPL got the lower graphs created and those Divs are empty without it. What I’m trying to do is simply get a click on those blue Market Cap and Sector links returning the right ID to change the page in the same manner I have working now with the stock symbols. I just want a value back of 101-104 for the top table and 111-121 for the lower table, integer or string as I am using a hidden ddc.Dropdown which uses strings instead.

I am really wondering if it has anything to do with the temperamentality of it not working if I changed id='storeStock' to any other value and how and why it works that way?

And one final note, I created three stores for ‘stock’, ‘etf’, and ‘index’ instead of using ‘stock’ for all three. That works great, but still doesn’t help the persistence. Hate to be using this as my support desk, but I’ve gone so far with this original callback and really appreciate the breakthrough. As far as persistence goes, I wonder if the if (i.reduce((a, b) => a + b, 0) > 0) logic is right? Once a value is set we do want to call this on every subsequent page change. Despite that, I have no idea why the persistence=True isn’t working as it always has been.

A nonexistent object was used in an `Input` of a Dash callback. The id of this object is `graph_syms_bell-segmenttemp` and the property is `hoverData`. The string ids in the current layout are: [url, session_id, ...]

This is related to the input not being available right at the startup, since this is expected behavior, you can should be able to stop this issue by passing at app initializtion:

app = dash.Dash(__name__, suppress_callback_exceptions=True)

(i.reduce((a, b) => a + b, 0) > 0) is to correctly identify actual clicks, and will get reloaded on every page load.

What page are you having the persistence issues? Most of your pages are now loading dynamically as far as I understand it. If your dynamic layout also includes those dropdowns, then they will not automatically load with persisted state. If you bring those elements outside of the dynamic part of the layout, then they should load as expected.

1 Like
from dash import dcc, html, no_update, Dash, Output, Input, State, page_container, ALL, ctx
import dash
import dash_bootstrap_components as dbc
from dash_bootstrap_templates import load_figure_template

app = Dash(__name__, use_pages=True,
           external_stylesheets=[dbc.themes.LITERA])
load_figure_template("bootstrap")


app.layout = html.Div([
    html.Div(dcc.Link('Home', href='/')),
    html.Div(dcc.Link('Stocks', href='/stocks')),
    html.Div(dcc.Store(id='storeStock')),
    page_container]
)

stockList = ['META', 'AAPL', 'AMZN', 'NFLX', 'GOOG', 'TSLA']


def layout_home():
    layout = html.Table([
        html.Tbody([
            html.Tr([html.Td(
                dcc.Link(sym, href='/stocks'),
                # dbc.Button(sym, href='/stocks', color='dark', className='text-white'),
                id={'index': sym, 'type': 'stock'}, n_clicks=0)]) for sym in stockList])
    ])
    return layout


def layout_stocks():
    layout = html.Div([
        html.H4('Stocks'),
        html.Div(dcc.Dropdown(id=f'dds_stock',
                              options=stockList,
                              persistence=True,
                              persistence_type='session',
                              style={'width': '100px'})),
        html.Div(id='hidden_stock_trigger'),
        html.Div(id='selected_stock')
    ])
    return layout


app.clientside_callback(
    """
        function (i) {
            if (i.reduce((a, b) => a + b, 0) > 0) {
                stock = JSON.parse(dash_clientside.callback_context.triggered[0].prop_id.split('.n_clicks')[0]).index
                return stock
            }
            return ''
        }
    """,
    Output('storeStock', 'data'),
    Input({'index': ALL, 'type': 'stock'}, 'n_clicks'),
    Prevent_Inital_Call=True
)

dash.register_page('Home', path='/', layout=layout_home())
dash.register_page('Stock Details', path='/stocks', layout=layout_stocks())


@app.callback(
    Output('selected_stock', 'children'),
    Input('dds_stock', 'value'),
)
def selected_stock(stock):
    print(f'selected_stock({stock})')
    return stock


# @app.callback(
#     Output('dds_stock', 'value'),
#     Input('hidden_stock_trigger', 'n_clicks'),
#     State('storeStock', 'data')
# )
# def stored_stock(v, stock):
#     print(f'stored_stock({stock})')
#     return stock


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

I have the problem demonstrated in the code listed above. Note that I improved your solution so that the selected stock is not simply printed on the page, but loads the dropdown menu which essentially constitutes that the stock is selected for the rest of the page to work.

In the example above, persistence is working just fine if you go back and forth from the home page to the stock page, but the home to stock linking doesn’t operate. After uncommenting the stored_stock callback, the linking works but persistence is gone when you travel to the Home page and back. You will also see it firing empty selected_stock calls on the page changes that clear the dropdown value.

Please let me know if you can find the right modification so the stored_stock isn’t clearing things. I would like it if your callback and selected_stock set storeStock, but then you get a duplicate output callbacks error.