Skip to main content

State system

The state system is the behavioral backbone of a Fission app.

This page is the reference version of that model. Use it when you want to confirm the contract for AppState, typed Action values, reducers, ReducerContext, or selectors without re-reading the more narrative Learn pages.

The core contract at a glance

PieceContractPurpose
AppStateYour durable application data modelHolds the product data the user interface describes and reducers update
ActionA typed, serializable intent value with a stable ActionIdDescribes what happened in app terms
reducerA function that updates state in response to an actionCentralizes state changes in one explicit path
ReducerContextReducer-time access to effect emission and action input metadataLets reducers request outside work and inspect returned input
Selector<S>A derived read-only view computationKeeps larger widgets from scattering raw state reads everywhere

AppState

AppState is the trait implemented by the application state type managed by the runtime.

In practice, your state is usually an ordinary Rust struct, and impl AppState for MyState {} marks it as the app's shared state model.

What belongs in AppState? Durable product truth. Text the user is editing, the selected screen, whether a save is in progress, the current locale preference, loaded data, error states, and other user-visible product state all fit here.

What does not belong there? Runtime-owned details such as current animation interpolation values, platform clipboard implementations, or other framework-managed interaction state.

Action

An Action is the typed message that describes user intent or some other meaningful event in the app.

The Action trait requires values to be serializable and to provide a stable ActionId through static_id(). In practice, most app actions are small Rust structs or enums that derive the framework macro and travel through the runtime as ActionEnvelope values.

The point of this system is not ceremony for its own sake. It gives the runtime a deterministic, typed way to route behavior across input, reducers, testing, diagnostics, and replay.

Use actions to describe what happened in app language. Examples include SaveRequested, LocaleChanged, DrawerClosed, or FilterUpdated.

Avoid using vague "do everything" actions that hide several unrelated transitions. Small, intention-revealing actions are easier to test and review.

Reducers

A reducer is the function that receives the current state, an action, and optionally reducer-time context, then updates state in response.

The modern reducer shape is:

fn handle(state: &mut MyState, action: MyAction, ctx: &mut ReducerContext<MyState>)

This is the important contract to remember: reducers are where state changes are supposed to happen.

That gives Fission its clear one-way flow:

  1. widgets emit actions,
  2. the runtime dispatches them,
  3. reducers update state,
  4. widgets rebuild from the new state.

Reducers should stay deterministic and explicit. They may request effects through ReducerContext, but they should not become ad hoc side-effect containers.

ReducerContext

ReducerContext is the reducer-time helper for two related jobs.

First, it exposes ctx.effects, which lets a reducer request jobs, services, commands, capabilities, and other runtime-managed work.

Second, it exposes ctx.input, which lets the reducer inspect the input data that accompanied this dispatch. That may include job results, capability results, service events, timer payloads, pointer data, or dropped-file data.

Use ReducerContext when a reducer needs to say "start this outside work" or "read the explicit data that came back from the runtime."

Do not treat ReducerContext as long-lived storage. It exists only while the reducer is running.

Binding reducers into the app

Two binding patterns matter most in ordinary app code.

Use with_reducer!(ctx, action, handler) inside build() when the action wiring is local to the widget tree. This is the most common pattern for buttons, text fields, and local interactions.

Use ActionRegistry and shell-side registration when reducers should be registered in a more centralized startup-style path.

The important architectural point is the same either way: state still changes through explicit reducer functions, not through hidden widget mutation.

Selectors

A selector is a read-only derived computation from View.

Publicly, Selector<S> exists so widgets can ask for exactly the view model they need instead of reaching into raw state fields everywhere. That becomes increasingly valuable as screens grow.

Use a selector when a widget needs formatted text, derived booleans, grouped data, or a small view model struct that is computed from current inputs.

Do not use a selector to hide side effects or mutate state. A selector is a read operation only.

In small examples, direct view.state reads are often fine. In larger apps, selectors help keep build code cleaner and make data dependencies easier to test.

A useful mental model for this page

AppState holds the durable data.

Action describes what happened.

Reducers decide how that changes state.

ReducerContext gives reducers explicit effect and input plumbing.

Selectors derive focused read models for widgets.

If you remember that shape, most of the reference pages around widgets, resources, input, and testing become much easier to navigate.

For async work and reducer-side effect emission, continue to Commands, services, and jobs and Resources and capabilities. For the widget-side contract that reads from this state system, see Widget trait.