Introducing simple-dash library to build app without callbacks

Hello everyone,

I’d like to show to the world the library I’m working on and gather feedback. The main point of the library is to allow user to bind the layout to data directly, thus avoiding need for callbacks altogether.

Let’s start with some example. We’ll create an app with single Input component, that renders back whatever the user has typed in.

from simpledash.callbacks import setup_callbacks

app.layout = html.Div([
    dash_core_components.Input(id='data-input', className='row'),
    
    # with Simple Dash you can use a dash dependency object directly in the layout
    # it will be replaced by actual `data-input.value` and updated every time it changes
    html.Div(dash.dependencies.Input('data-input', 'value'), className='row', id='output-div')
])

setup_callbacks(app)  # this will scan the layout and create all necessary callback functions

and that’s it! No callbacks needed, the library has taken care of everything.

This was obviously extremely simple, but with simpledash you’re able to define much more complex operations than this, so let’s see another example. We would like to have two Inputs (named A and B) and let user decide which one is going to be used for rendering the output value. This choice will be done via dropdown.

import dash_core_components as dcc
from dash.dependencies import Input
from simpledash.data.data_providers import data_provider

input_a = dcc.Input(id='data-input-a', value="")
input_b = dcc.Input(id='data-input-b', value="")
input_chooser = dcc.Dropdown(id='input-chooser', options=options_from(['A', 'B']))

# use a `data_provider` annotation to indicate, that the method provides data based on inputs
# inputs used by function are declared as arguments to the decorator
# and are later on passed to the function as arguments
@data_provider(Input('data-input-a', 'value'), Input('data-input-b', 'value'), Input('input-chooser', 'value'))
def output_value(input_a_value, input_b_value, input_chooser_value):
    if input_chooser_value == "A":
        return input_a_value
    if input_chooser_value == "B":
        return input_b_value
    return ""


app.layout = html.Div([
    html.Div(["A: ", input_a], className='row'),
    html.Div(["B: ", input_b], className='row'),
    html.Div(["Which one to show? ", input_chooser], className='row'),
    html.Br(),
        # you simply use `data_provider` instance in the layout (as in previous example)
        html.Div(["Here's the output: ", output_value], className='row', id='output'),
        # but you can also run some simple operations on `data_provider`, like `upper()`
        html.Div(["Also in uppercase!: ", output_value.upper()], className='row', id='output-upper')
])

setup_callbacks(app)

This time we have used data_provider decorator to declare the function that is able to provide data based on inputs. Note, that this is a plain python function, so you should be able to do any operation on inputs, regardless of the complexity.

Interesting thing we see in the example is output_value.upper(). This is just a syntax sugar
that Simple Dash gives you - instead of writing another data_provider to do the uppercasing,
we can call the method directly on output_value (and this will create new data_provider under the hood for you).

Please note that the set of operations you are able to do on data_provider instance are limited to:

  • accessing the property (output_value.xyz)
  • accessing the item by index (output_value['xyz'])
  • calling the method (output_value.xyz("param"))

For even more complex operations, you are able to nest data_providers, but to see an example let me redirect you to the README.

This is the very first release of the library, so mostly I’m interested in validation of the idea. What do you think of it? Would you be keen to use this kind of API vs callbacks?

I’ve been user-testing the lib on some people in my company and the initial feedback is very positive - people find it more intuitive to build an app with simpledash rather than plain dash (the caveat here is those people didn’t have much experience with dash previously, so maybe that’s why callbacks sounded strange to them. I wonder what power-user of dash would think - please, help me out).

Anyways, here’s the link to the repo: https://github.com/rtshadow/simple-dash
I’m very keen to hear your feedback.

Thanks a lot,
Przemek

1 Like

Hi @pastuszka.przemyslaw ,

I haven’t tried it out yet, but in terms on immediate impression, i really like the syntax. Being a regular Dash user, it took a few minutes for me to grasp it, but now it seems pretty clear. I will post some more feedback, when i have tried it out in practice.

\emher

Very nice!

A few questions:

  1. How do you deal with the use case of having callbacks that return components with their own callbacks? This is one area that I found tricky when considering an API like this - If the outputs are embedded within the components and returned dynamically, then it’s not clear to me how you would register these callbacks in advance. Here’s the canonical multi-page app example:
app = dash.Dash(__name__, suppress_callback_exceptions=True)

app.layout = html.Div([
    dcc.Location(id='url', refresh=False),
    html.Div(id='page-content')
])

def page_1_layout():
    return html.Div([
        dcc.Input(id='page-1-input'), 
        html.Div(id='page-1-output')
    ])


@app.callback(Output('page-content', 'children'),
              [Input('url', 'pathname')])
def display_page(pathname):
    if pathname == '/page-1':
         return page_1_layout()

@app.callback(Output('page-1-output', 'children'), [Input('page-1-input', 'value')])
def update_output(value):
    return value
  1. Have you worked on a syntax for multi-output callbacks? There is a performance tradeoff between having multiple callbacks that update multiple outputs (which could run in parallel in production) vs a single callback that updates multiple outputs, and so it’s important for a user to have control over how they structure these updates.

Thanks for the comments @Emil and @chriddyp.

As for 1) - it’s a very good challenge! This is currently not supported by the library, but here’s a quick idea how I’d do it:

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

page_1_layout = html.Div([
    dcc.Input(id='page-1-input'),
    html.Div(Input('page-1-input', 'value'), id='page-1-output')
])


@data_provider(Input('url', 'pathname'))
def display_page(pathname):
    if pathname == '/page-1':
        return page_1_layout


app.layout = html.Div([
    dcc.Location(id='url', refresh=False),
    html.Div(children=display_page, id='page-content')

])

# version A
setup_callbacks(app, [app.layout, page_1_layout])

# version B
setup_callbacks(app, app.layout)
setup_callbacks(app, page_1_layout)

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

so basically you need to provide setup_callbacks all the layouts it needs to scan (previously it was scanning the app.layout only). I will add this possibility in next release.

As for the 2) - currently the implementation is one callback per component, but I do have possibility of updating many outputs for many components within single callback in mind. I didn’t pay much attention to performance yet as the priority was to get the API straight.
Do you think multi-output callbacks vs single-output callback choice would affect API design in any way? For me it looks like something I can hide under the hood.

Ah I see. Yeah, that’s tough. In some cases, permuting through all of the different layouts in advance will likely be too expensive or even too hard to program! Imagine def page_1_layout() that runs a different set of components depending on some internal conditional logic. Or imagine that def page_1_layout() is making an API call or performing a computation and that there are tens of different pages!

I think it would, because you would want the end user to have explicit control over whether they write multiple functions or a single function.

This scenario can be solved by extracting the API call itself to separate data_provider - this way the layout is static. So it’ll look sth like this:

@data_provider()
def expensive_data():
    return "External api call"

page_2_layout = html.Div([
    html.Strong(expensive_data, id='expensive-data')
])
setup_callbacks(app, page_2_layout)

This doesn’t work in current version of the lib, because the code will try to create a callback with no inputs (but I believe there are workarounds)

For all the apps I’ve seen so far, the layout was static (except multi-paging). But I haven’t seen that many. Can you please share some real-world use case for this?

OK, I think I finally get what you mean.
Initially I wasn’t planning on giving the user such control - the design of the API was with “casual” users in mind, who wouldn’t bother to fine-tune the app anyway (so the lib can make an arbitrary choice how to handle callbacks).

Here are some quick ideas how to give user such control:

  1. Make sure it’s safe to use both simpledash and callbacks in one app. Use simpledash where the default suits you and fall back to callbacks if necessary.
  2. Allow user to choose a global strategy how callbacks will be handled, so sth like setup_callbacks(app, layout=app.layout, callback_strategy=SINGLE_CALLBACK_PER_OUTPUT)
  3. Allow user to choose a strategy for any subtree in a layout. Here’s what I mean:
from simpledash.callbacks.strategies import SingleCallbackPerOutput

app.layout = html.Div([
    dcc.Input(id='y-column', value='y'),
    SingleCallbackPerOutput(
        dcc.Graph('graph', figure=dict(
            data=[
                dict(x=data['x'],
                     y=data[Input('y-column', 'value')],
                     customdata=data.index,
                     mode='markers')
            ]
        ))
    )
])

so we’ve used SingleCallbackPerOutput wrapper to indicate that whole Graph should use different strategy than default one (which is single callback per component).

Surely there are other ways - if you’ve got any idea, please do share.

1 Like

FYI,
I have just created a new release that adds the functionality, we’ve been talking about:

  • setup_callbacks now takes an optional layout parameter, which will allow you to setup the callbacks for multipage apps
  • you can now use data_provider with no inputs - this allows you to compute some data lazily (because data_provider will be executed only if the component is shown to the user)
1 Like