dash icon indicating copy to clipboard operation
dash copied to clipboard

Remove Callback Wildcard Restrictions (MATCH)

Open milind opened this issue 1 year ago • 15 comments

Is your feature request related to a problem? Please describe. In trying to make my applications as modular as possible, I tend to use pattern-matching callbacks just about everywhere. There are many instances, however, where I need to be able to detect which component was triggered (via MATCH) and update a universal component.

Describe the solution you'd like I would love for the restriction on the MATCH wildcard that requires the same number of MATCH values in the Input ID's and Output ID's to be lifted, allowing for more wildcards in the input than the output. It's my understanding that today's release of allow_duplicate in Dash's Output component should open the door to adding in the aforementioned solution.

Describe alternatives you've considered The current solution for this use case is to use the ALL wildcard along with the callback context to filter down to the content that I'm ultimately interested in, but that solution does not scale especially well as it requires a lot of information to pass through the network, slowing things down.

milind avatar Mar 16 '23 22:03 milind

Thanks @milind

@T4rk1n I think "any MATCH present in the inputs must be present in all the outputs too" is another flavor of the restriction "every output can be connected to only one callback" - it's just that these duplicates come from the same function. So in the same way as we do for multiple callback functions we should be able to relax that restriction when the output that's missing a MATCH specifies allow_duplicate. Seem reasonable?

To make this concrete, I guess we're talking about callbacks that would look like:

@callback(
    Output("out", "children"),
    Input({"id": MATCH}, "n_clicks"),
    State({"id": MATCH}, "id"),
    prevent_initial_call=True
)
def cb(_, _id):
    return f"you clicked the button with id {_id['id']}"

alexcjohnson avatar Mar 16 '23 23:03 alexcjohnson

I think the validation should be lifted entirely for Output, it makes MATCH awkward to use and I remember removing the validation without any breakage.

T4rk1n avatar Mar 17 '23 16:03 T4rk1n

It would have exactly the same kind of breakage scenarios as other duplicate outputs, an ambiguity when two callbacks result from the same stimulus. Which is why I think allowing it the same way (with allow_duplicate) makes most sense.

alexcjohnson avatar Mar 17 '23 20:03 alexcjohnson

I would also like this restriction to be lifted. I'm trying out a convention where callbacks which modify database state triggers a re-render using a "trigger" output. This does not work with this restriction in place.

For searchability - this is the error message:

`Input` / `State` wildcards not in `Output`s

or

Output X
does not have MATCH wildcards on the same keys as
Output Y
MATCH wildcards must be on the same keys for all Outputs. ALL  wildcards need not match, only MATCH

Someone else mentioned that they would like to do partial update based on a MATCH callback. On the surface this also sounds like a reasonable use-case (with the disclaimer that I haven't used partial update enough to really know it makes sense).

EDIT: Thinking a bit further - any scenerio where doing something to a component[1] should trigger a not-local-to-that-component update become cumbersome to implement. Only workaround (other than the one mentioned by OP) I can see is to add dummy outputs per component and route these to the callback implementing the global update.

[1] Which there can be an arbitrary number of

olejorgenb avatar Sep 14 '23 18:09 olejorgenb

A current workaround to avoid the network cost of passing ALL to the callback would be:

  • to use ALL in a clientside callback, storing the information about the triggered input(s) at a level with as many MATCH as the desired Output
  • use the above store in a serverside callback to update the desired Output

Something like:

clientside_callback(
    """() => {
        return dash_clientside.callback_context.triggered.map(t => JSON.parse(t.prop_id.split(".")[0]))
    }"""
    Output("input_store", "data"),
    Input({"type": "btn", "id": ALL}, "n_clicks"),
)

@callback(
    Output("output", "children"),
    Input("input_store", "data"),
)
def myfunc(triggered_inputs_data):
    # ...

RenaudLN avatar Nov 02 '23 21:11 RenaudLN

I would appreciate this restriction lifted too, as it would simplify my callbacks. I was so surprised it doesnt work when I tried to write similiar callback as you did.

pitris90 avatar Feb 07 '24 20:02 pitris90

I second the request, as this limitation if often problematic for our use cases.

wilecoyote2015 avatar Feb 20 '24 09:02 wilecoyote2015

I agree. The current behavior is counterintuitive, as you can have the same output in multiple callbacks, and this would simplify many patterns.

alvarodemig avatar Mar 06 '24 23:03 alvarodemig

Any updates on this?

mmarfat avatar Apr 03 '24 09:04 mmarfat

Any updates on this?

lunathanael avatar Apr 16 '24 20:04 lunathanael

Plotly team is looking into this as part of a future Dash release!

ndrezn avatar Apr 21 '24 17:04 ndrezn

Another example use case:

@callback(
    Output("notifications_container", "children", allow_duplicate=True),
    Output({"id": MATCH, "type": "grid"}, "rowData", allow_duplicate=True),
    Input({"id": MATCH, "type": "grid"}, "cellValueChanged")
)
def on_edit_grid_callback(event):
    return on_edit_grid(event)

deadkex avatar May 15 '24 07:05 deadkex

A current workaround to avoid the network cost of passing ALL to the callback would be:

  • to use ALL in a clientside callback, storing the information about the triggered input(s) at a level with as many MATCH as the desired Output
  • use the above store in a serverside callback to update the desired Output

Something like:

clientside_callback(
    """() => {
        return dash_clientside.callback_context.triggered.map(t => JSON.parse(t.prop_id.split(".")[0]))
    }"""
    Output("input_store", "data"),
    Input({"type": "btn", "id": ALL}, "n_clicks"),
)

@callback(
    Output("output", "children"),
    Input("input_store", "data"),
)
def myfunc(triggered_inputs_data):
    # ...

Actually, this works really well for me! I just want to know the ID of the button that's clicked, in an efficient way. Awesome 👍👍

mccarthysean avatar Jun 03 '24 22:06 mccarthysean

There is a new workaround for outputting with MATCH callback to unmatched ids with the new set_props.


@callback(
    Output({"id": MATCH, "type": "grid"}, "rowData", allow_duplicate=True),
    Input({"id": MATCH, "type": "grid"}, "cellValueChanged")
)
def on_edit_grid_callback(event):
    set_props("notifications_container", {"children": f"edited: {ctx.triggered_id}"})
    return on_edit_grid(event)

T4rk1n avatar Jun 17 '24 15:06 T4rk1n