Adding Component via Clientside Callbacks

Good day, All,

I recently encountered an issue where I was wanting to give feedback to my clients after immediately interacting with the application, starting a process that could take a bit. One way that I found that is really nice is the Notification by DMC.

The problem with using a notification, when passing back a component it needed to have a response from the server, meaning a roundtrip in order to complete the task. This isnt a big issue if you are really close to the server, but create a gap and you will notice a huge lag in the button push and the desired feedback that they triggered an event and process.

It is also an issue if you have a keydown listener and the user doesnt know that they actually triggered the event, thus potentially stacking the events. :partying_face:

Anyways, I decided to tackle a question that has been bugging me, is it possible to provide a component in a clientside callback.

Short answer, yes. Longer answer, there are a few ways that we can achieve this.

First thing, we need to know that the component language changes a bit:

html.Div("test"){'props': {'children': 'test'}, 'type': 'Div', 'namespace': 'dash_html_components'}

A cool thing to see this is using to_plotly_json, eg:

html.Div("test").to_plotly_json()

Using this cool feature, we can actually just insert this into a clientside callback like this:

app.clientside_callback(
    """function (n) {
        if (n) {
            return """ + json.dumps(html.Div("test").to_plotly_json()) + """
        }
        return window.dash_clientside.no_update
    }""",
    Output("test", "children"), Input("testing", "n_clicks")
)

^ this works, but isnt really dynamic, taking this same example and the above string from we can do this:

app.clientside_callback(
    """function (n) {
        if (n) {
            return {'type': 'Div', 'namespace': 'dash_html_components', 'props': {'children': n}}
        }
        return window.dash_clientside.no_update
    }""",
    Output("test2", "children"), Input("testing2", "n_clicks")
)

Now, as we click, the number inside the other component will increase.

if we compare this to regular callback on the testing2, we have a .5 second fuse:

@app.callback(
    Output("test3", "children"), Input("testing2", 'n_clicks')
)
def serverCall(n):
    if n:
        time.sleep(.5)
        return [html.Div(n)]
    return no_update

Obviously, this fails in giving immediate feedback, in the meantime users could have issues or try again.

image

Here is this example:

from dash import html, Output, Input, Dash, no_update
import json
import time

app = Dash(__name__)
app.layout = html.Div(
    [
        html.Button('testing', 'testing'), html.Button('testing2', 'testing2'),
        html.Div(id='test'), html.Div(id='test2'), html.Div(id='test3')
    ]
)

app.clientside_callback(
    """function (n) {
        if (n) {
            return """ + json.dumps(html.Div("test").to_plotly_json()) + """
        }
        return window.dash_clientside.no_update
    }""",
    Output("test", "children"), Input("testing", "n_clicks")
)

app.clientside_callback(
    """function (n) {
        if (n) {
            return {'type': 'Div', 'namespace': 'dash_html_components', 'props': {'children': n}}
        }
        return window.dash_clientside.no_update
    }""",
    Output("test2", "children"), Input("testing2", "n_clicks")
)

@app.callback(
    Output("test3", "children"), Input("testing2", 'n_clicks')
)
def serverCall(n):
    if n:
        time.sleep(.5)
        return [html.Div(n)]
    return no_update

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

This is cool, but lets take a more real life example, have fun with notifications:

from dash import html, Output, Input, Dash, no_update
import json
import dash_mantine_components as dmc
from dash_iconify import DashIconify
import time

app = Dash(__name__)
app.layout = html.Div(
    [
        dmc.NotificationsProvider([
            html.Button('testing', 'testing'), html.Button('testing2', 'testing2'),
            html.Div(id='test'), html.Div(id='test2')
        ])
    ]
)

app.clientside_callback(
    """function (n) {
        if (n) {
            return [""" + json.dumps(
                dmc.Notification(id="test3", action='show', message='this is a test',
                                 autoClose=False, loading=True, color='orange').to_plotly_json()) + """, true]
        }
        return [window.dash_clientside.no_update, window.dash_clientside.no_update]
    }""",
    Output("test", "children"), Output("testing", "disabled"),
    Input("testing", "n_clicks"), prevent_initial_call=True
)

@app.callback(
    Output("test", "children", allow_duplicate=True),
    Output("testing", "disabled", allow_duplicate=True),
    Input("testing", "n_clicks"), prevent_initial_call=True
)
def serverCall(n):
    if n:
        time.sleep(2)
        return dmc.Notification(id='test3', action='update', message='test complete',
                         icon=DashIconify(icon='akar-icons:circle-check'), color='green'), False
    return no_update, no_update

app.clientside_callback(
    """function (n) {
        if (n) {
            return [{'type': 'Notification', 'namespace': 'dash_mantine_components', 
            'props': {'id': `${n}`, 'action': 'show', message: `this is a test ${n}`, color: 'orange', loading: true,
            autoClose: false}}, true]
        }
        return [window.dash_clientside.no_update, window.dash_clientside.no_update]
    }""",
    Output("test2", "children"), Output("testing2", "disabled"), Input("testing2", "n_clicks"), prevent_initial_call=True
)

@app.callback(
    Output("test2", "children", allow_duplicate=True),
    Output("testing2", "disabled", allow_duplicate=True),
    Input("testing2", "n_clicks"), prevent_initial_call=True
)
def serverCall2(n):
    if n:
        time.sleep(2)
        return dmc.Notification(id=f'{n}', action='update', message=f'test {n} complete',
                         icon=DashIconify(icon='akar-icons:circle-check'), color='green'), False
    return no_update, no_update

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

image


edit: Yes, these clientside returned components can receive updates from dash:

from dash import html, Output, Input, Dash, no_update
import json
import time

app = Dash(__name__, suppress_callback_exceptions=True)
app.layout = html.Div(
    [
        html.Button('testing', 'testing'), html.Button('testing2', 'testing2'), html.Button('testing4', 'testing4'),
        html.Div(id='test'), html.Div(id='test2'), html.Div(id='test3')
    ]
)

app.clientside_callback(
    """function (n) {
        if (n) {
            return """ + json.dumps(html.Div(id="test4", children="test").to_plotly_json()) + """
        }
        return window.dash_clientside.no_update
    }""",
    Output("test", "children"), Input("testing", "n_clicks")
)

app.clientside_callback(
    """function (n) {
        if (n) {
            return {'type': 'Div', 'namespace': 'dash_html_components', 'props': {'children': n}}
        }
        return window.dash_clientside.no_update
    }""",
    Output("test2", "children"), Input("testing2", "n_clicks")
)

app.clientside_callback(
    """function (n) {
        if (n) {
            return {'type': 'Div', 'namespace': 'dash_html_components', 'props': {'children': n}}
        }
        return window.dash_clientside.no_update
    }""",
    Output("test4", "children"), Input("testing4", "n_clicks")
)

@app.callback(
    Output("test3", "children"), Input("testing2", 'n_clicks')
)
def serverCall(n):
    if n:
        time.sleep(.5)
        return [html.Div(n)]
    return no_update

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

image

12 Likes

@jinnyzor thanks for sharing. This is interesting.

I ran the last code snippet that you shared; I have a quick question.

If you load the app and click twice on the testing4 button, then click the testing button, you will immediately see the div content as 2. This is presumably because the testing4 button was clicked twice.

But why is that happening? When I click the testing button, I expected the children to say test because that’s what’s in the clientside callback.

Thanks @adamschroeder!

This is most likely due to how plotly dash callbacks work, the first time that the component is rendered, it triggers callbacks that are rendered to it.

This is an issue that often arises from pattern-matching callbacks where components are rendered dynamically.

Preventing the initial call only works when the layout is initially loaded and not when a component is being added after the layout has rendered.

Again, this is just a guess, but something I have ran into with pattern-matching callbacks.

1 Like

This is interesting!
I’m struggling to solve a similar issue. Instead of returning a dash component from a client side callback, I’m trying to render a dash component dynamically from within a dash custom component. I am calling this function (defined in assets/funcs.js) to create the component.

function(data, onChange) {
            var i = {type:"btn", index: crypto.randomUUID()};
            var comp = React.createElement(window.dash_html_components.Button, {
                id: JSON.stringify(i),
                setProps: (props) => {
                    return props
                }
            }, children="hello");
            console.log(comp);
            return comp
        }

The component renders fine. However, I have a feeling that this is not the right way to do it. Also, i’m unable to find these components in callbacks. Any clue why this may be happening?

To use pattern-matching callbacks, the component needs to be added with the id in a stringified fashion. Dash uses a function called stringifyId, which hopefully they will expose sometime soon.

Anyways, this function looks like this:

function stringifyId(id) {
    if (typeof id !== 'object') {
        return id;
    }
    const stringifyVal = (v) => (v && v.wild) || JSON.stringify(v);
    const parts = Object.keys(id)
        .sort()
        .map((k) => JSON.stringify(k) + ':' + stringifyVal(id[k]));
    return '{' + parts.join(',') + '}';
}

To use a button, I’d also pass n_clicks first:

function(data, onChange) {
            var i = {type:"btn", index: crypto.randomUUID()};
            var comp = React.createElement(window.dash_html_components.Button, {
                id: stringifyId(i),
                n_clicks: 0,
                setProps: (props) => {
                    return props
                }
            }, children="hello");
            console.log(comp);
            return comp
        }

Also, if dash is not the one rendering the component, via a callback, then it is not going to be monitored by dash.


Or maybe try without stringifying the id.

Thanks for the reply. I had tried using an string id as well and that also did not work.

I’m guessing this is probably the reason. Wonder if there is some way to make dash aware of a DOM element that it did not create.

Nope, this is not currently available.

You might be able to combine the logic of an AIO component to create a clientside callback that adds the component for you though.

1 Like

i’m not sure if my current usecase will fit in an AIO component, but I’ll give it a shot.

Hey,
I encountered an issue while working with nested Dash components and the .to_plotly_json() method. It seems that when dealing with nested components, the .to_plotly_json() method alone doesn’t handle the conversion correctly.

I wrote a function that recursively converts nested components into the appropriate format using .to_plotly_json():

def recursive_to_plotly_json(component):
    if hasattr(component, 'to_plotly_json'):
        component = component.to_plotly_json()
        children = component['props'].get('children')
        
        if isinstance(children, list):
            component['props']['children'] = [recursive_to_plotly_json(child) for child in children]
        else:
            component['props']['children'] = recursive_to_plotly_json(children)
   
    return component
5 Likes