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:
- an implementation with an action's enum and a payload (before)
- an implementation with an actions classes (after)
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 :
- linters to alert you when you miss-typed on the read part
- linters to alert you when you miss-typed on the "create" part
- automated refactorings
- auto-completion
- typing
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