Name what your app can do
Replace scattered callbacks with typed actions and one central update function while keeping every change easy to follow.
Outcome: buttons and a selection control send typed intentions through one predictable update function.
01
Follow an action from widget to state
The pieces are small on purpose. Together they make every user intention explicit and keep mutations in one place.
1. Register the action type
A copyable marker implements ActionSchema and links the application to its action enum at compile time.
2. Describe intentions
CounterAction names Increment, Reset, and SelectPace instead of hiding those choices inside unrelated callbacks.
3. Dispatch from controls
Button::on_click sends a variant. SegmentedControl::on_change maps the selected value into another variant.
4. Update in one place
AppBuilder::update receives mutable state, mutable services, and one action, then returns any follow-up Commands.
CounterActionupdate(...)State02
Build the typed action path
Build typed actions in three passes instead of absorbing the whole application at once.
Build it in small steps
Each code block changes one idea from the verified reference application.
Step 1: name the intentions
The enum lists every event the example accepts, and ActionSchema tells the app which enum to route.
#[derive(Clone)]
enum CounterAction {
Increment,
Reset,
SelectPace(Pace),
}
#[derive(Clone, Copy)]
struct CounterActions;
impl ActionSchema for CounterActions {
type Action = CounterAction;
}Step 2: centralise the decisions
update receives one action, changes the relevant State, and returns any follow-up Commands.
fn update(
state: &mut CounterState,
_services: &mut (),
action: CounterAction,
) -> Commands<CounterAction> {
match action {
CounterAction::Increment => state.count.update(|value| *value += 1),
CounterAction::Reset => state.count.set(0),
CounterAction::SelectPace(pace) => state.pace.set(pace),
}
Commands::none()
}Step 3: let controls emit actions
The segmented control and buttons propose actions. They do not mutate application State themselves.
SegmentedControl::new()
.option(Pace::Calm, "Calm")
.option(Pace::Focused, "Focused")
.selected(view_state.pace.clone())
.on_change(CounterAction::SelectPace),
)
.child(
Row::new()
.gap(12.0)
.child(
Button::with_label("Add one")
.on_click(CounterAction::Increment),
)
.child(
Button::with_label_variant("Reset", ButtonVariant::Secondary)
.on_click(CounterAction::Reset),
),Compiled with Rust 1.88 against beta.2. ActionSchema, AppBuilder::state, actions, update, Commands, Button, and SegmentedControl are public.
03
Why the state contains State handles
AppBuilder owns ordinary application state. The example stores clonable State handles inside it so the central update function and the window content share the same visible values.
Commands::none is still meaningful
The update function has no delayed or follow-up action to schedule here. Updating the State handles already notifies their retained readers.
Try it yourself
Exercise: add one typed intention
Keep every counter mutation inside the central update function.
- 1Add Decrement to the action enum and handle it with saturating_sub in update.
- 2Connect a secondary button to the new action.
The button only emits Decrement. It should not receive or clone the count State.
The new button changes the count, zero never underflows, and all mutations still live in update.