Debugging Dash clientside callback

I have the following Dash app (simplified). It displays text boxes and a list of words (buttons). You click the buttons that have text to add the corresponding text to the text box (this is the part I want a client-side callback for). There is another callback with the same output (now that that’s permitted with Dash 2.9) which triggers when the user clicks the “->” button which refreshes the terms.

import re

import dash
import dash_bootstrap_components as dbc
import pandas as pd
from dash import dcc, html
from dash.dependencies import ALL, Input, Output, State
from dash.exceptions import PreventUpdate

EMPTY_DIV = html.Div()

# Div for sample list of terms
WORDS = list(pd.read_csv('font_terms.csv')['adj'])
word_items = [html.Li(children=dbc.Button(word, id={'type': 'fill-word-button', 'index': i})) for i, word in enumerate(WORDS)]
terms_div = html.Div(id='terms', children=word_items)


app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP], prevent_initial_callbacks="initial_duplicate")
server =  app.server

app.layout = html.Div([
    dcc.Store(id='terms-store', data=WORDS),
    html.Div(id="adj-inputs", className="column",
            children= [dcc.Input(id={"type": "adj-input", "index": i}, value='') for i in range(5)]),
    terms_div,
    html.Button("→", id='forward-button', n_clicks=0),])


@app.callback(
    [Output({"type": "adj-input", "index": i}, 'value') for i in range(5)],
    Input({"type": "fill-word-button", "index": ALL}, "n_clicks"),
    State({'type': 'adj-input', 'index': ALL}, "value")

)
def fill_word_button(button_click, adj_inputs):
    ctx = dash.callback_context
    if not ctx.triggered:
        raise PreventUpdate
    button_id = ctx.triggered[0]['prop_id'].split('.')[0]

    button_index = int(re.search(r"\d+", button_id).group())
    # check if there is an empty text box
    if '' not in adj_inputs:
        raise PreventUpdate
    adj_inputs = ['' if a is None else a for a in adj_inputs] + [WORDS[button_index]]
    adj_inputs = list(set([a for a in adj_inputs if a!=''])) + [a for a in adj_inputs if a=='']
    return adj_inputs[:5]


# The following callback, once debugged, should replace the callback above ("fill_word_button")
# @app.clientside_callback(
#     """
#     function(n_clicks, adj_inputs, words) {
#         const ctx = dash_clientside.callbackContext;
#         if (!ctx.triggered.length) {
#             return adj_inputs;
#         }
#         const button_id = ctx.triggered[0]['prop_id'].split('.')[0];
#         const button_index = parseInt(button_id.match(/\d+/)[0]);
#         // check if there is an empty text box
#         if (adj_inputs.every(a => a !== '')) {
#             return adj_inputs;
#         }
#         adj_inputs = adj_inputs.concat([words[button_index]]);
#         adj_inputs = [...new Set(adj_inputs.filter(a => a !== '')), ...adj_inputs.filter(a => a === '')];
#         return adj_inputs.slice(0, 5);
#     }
#     """,
#     Output({"type": "adj-input", "index": ALL}, 'value'),
#     Input({"type": "fill-word-button", "index": ALL}, "n_clicks"),
#     State({'type': 'adj-input', 'index': ALL}, "value"),
#     State('terms-store', 'data')
# )


@app.callback(

    [Output({"type": "adj-input", "index": i}, 'value', allow_duplicate=True) for i in range(5)],
    [Input("forward-button", "n_clicks")],
    [State({'type': 'adj-input', 'index': ALL}, "value")],
)
def refresh_terms(forward_clicks, adj_inputs):
    ctx = dash.callback_context
    if not ctx.triggered:
        raise PreventUpdate
    button_id = ctx.triggered[0]['prop_id'].split('.')[0]

    if button_id == "forward-button":
        adj_inputs = [adj for adj in adj_inputs if adj !=""]
        # Do something with the adj_inputs e.g. upload to database and
        # db.insert_response_mysql(adj_inputs)
        # Reset adj_inputs
        return [""]*5


if __name__ == "__main__":
    app.run_server(debug=True, port=8000, host="0.0.0.0")

The error I have is connected to the refresh_terms callback

[State({'type': 'adj-input', 'index': ALL}, "value")],
TypeError: 'NoneType' object is not callable

This only happens when I try using the client-side callback but is fine when I use a separate callback. Please help me debug this error.

While reviewing your code, I noticed a commented line that seems faulty.
The line mentions the use of “@app.clientside_callback”, which is incorrect syntax for defining a clientside callback in Dash. Instead, the correct syntax is without the “@” prefix: