How to improve expressiveness in a flux/redux Dash

We've seen how to use the flux/redux architecture with Dash. Then how to refactor a common dash app, to a dash app that use a flux/redux pattern. Without breaking it. Let's dig on how to improve expressiveness and type-safety in a flux/redux architecture with Dash.

The idea is to replace the couple (action's enum, payload) by class actions.

The demo app

I developed a minimal app that compare the two approaches.

It's a single-page app that allows you to send a message to a city:

The code is available here. It contains 2 apps:

Breaking down of the implementation

On the state management side,

# state.py

class Action(Enum):
    SETUP_CITY = auto()
    ...


def reduce(state: dict, action: Action, payload=None) -> dict:
    if action == Action.SETUP_CITY:
        return state | {
            "city-dropdown": {
                "options": payload["options"],
                "value": payload["value"],
                "disabled": len(payload["options"]) == 0,
            },
        }

    ...

becomes

# state.py

class Action:
    pass


@dataclass(frozen=True)
class SetupCity(Action):
    options: tuple[str, ...]
    value: str | None

    @classmethod
    def reset(cls) -> SetupCity:
        return SetupCity(tuple(), None)

    @classmethod
    def initialize(cls, options: tuple[str, ...]) -> SetupCity:
        return SetupCity(options, None)

...

def reduce(state: dict, action: Action) -> dict:
    if isinstance(action, SetupCity):
        return state | {
            "city-dropdown": {
                **state["city-dropdown"],
                "options": action.options,
                "value": action.value,
                "disabled": len(action.options) == 0,
            },
        }

    ...

The payload parameter disappear. Its content is now embodied in the action.

You don't access the payload's values through hardcoded values anymore (like payload["options"]). You reference the action's attributes (like action.options). It allows :

On the callback side,

# app.py

from state import INITIAL_STATE, reduce, Action

...

@app.callback(
    Output("state", "data", allow_duplicate=True),
    Input("country-dropdown", "value"),
    State("state", "data"),
    prevent_initial_call=True,
)
def on_select_country(selected_country: str | None, state: dict):
    if selected_country == state["country"]:
        return dash.no_update

    cities = db.get_cities(selected_country) if selected_country is not None else []

    state = reduce(state, Action.SELECT_COUNTRY, selected_country)
    state = reduce(state, Action.SETUP_CITY, {"options": cities, "value": None})
    return state

...

@app.callback(
    Output("state", "data"),
    Input("send-button", "n_clicks"),
    State("state", "data"),
    prevent_initial_call=True,
)
def on_click_send(n: int | None, state: dict):
    if n is None:
        return dash.no_update

    state = reduce(state, Action.SEND_MESSAGE)
    state = reduce(state, Action.SELECT_COUNTRY, None)
    state = reduce(state, Action.SETUP_CITY, {"options": [], "value": None})
    state = reduce(state, Action.SET_MESSAGE, "")
    return state

becomes

# app.py

from state import (
    INITIAL_STATE,
    reduce,
    SetupCity,
    SelectCountry,
    SelectCity,
    SetMessage,
    SendMessage,
)

...

@app.callback(
    Output("state", "data", allow_duplicate=True),
    Input("country-dropdown", "value"),
    State("state", "data"),
    prevent_initial_call=True,
)
def on_select_country(selected_country: str | None, state: dict):
    if selected_country == state["country"]:
        return dash.no_update

    cities = db.get_cities(selected_country) if selected_country is not None else []

    state = reduce(state, SelectCountry(selected_country))
    state = reduce(state, SetupCity.initialize(cities))
    return state

...

@app.callback(
    Output("state", "data"),
    Input("send-button", "n_clicks"),
    State("state", "data"),
    prevent_initial_call=True,
)
def on_click_send(n: int | None, state: dict):
    if n is None:
        return dash.no_update

    state = reduce(state, SendMessage())
    state = reduce(state, SelectCountry.reset())
    state = reduce(state, SetupCity.reset())
    state = reduce(state, SetMessage.reset())
    return state

Usage of hard-coded values is reduced, and actions factory methods brings expressiveness.

Leave a reply