State system

The state system is the behavioral backbone of a Fission app. It separates durable product state, local widget memory, build-scope context, and reducer-side effects so the same app model can run through different shells without hidden platform coupling.
For a step-by-step guide, read State, handles, and providers. This page is the reference contract.

Core pieces

Piece
Contract
Purpose
GlobalState
Runtime-owned durable app state
Holds product truth shared across screens, reducers, tests, and shells
StateField<T>
Handle to retained local widget state
Stores UI-local memory for one stable widget identity
BuildCtxHandle<S>
Build-scope wiring handle
Binds actions, local reducers, resources, portals, media, and animations
ViewHandle<S>
Build-scope read handle
Reads GlobalState, Env, runtime values, layout snapshots, and selectors
Provider<T, F>
Typed build-stack value scoped to descendants
Lets parents inject contextual read-only values without prop drilling
Action
Typed, serializable intent with stable ActionId
Describes what happened in app terms
reducer
Function that updates state for an action
Centralizes mutations in one explicit path
ReducerContext<S>
Reducer-time effect and input context
Lets reducers request outside work and read returned payloads
Selector<S>
Read-only derived view computation
Keeps component conversion focused on view-ready values

GlobalState

GlobalState is implemented by the root product state managed by the runtime.
A GlobalState type must be 'static, downcastable, Send, Sync, and Debug. Common primitive types and () implement it already. App structs usually derive or implement it directly.
#[derive(Debug, Default, Clone, FissionGlobalState)]
pub struct ShopState {
    pub cart: CartState,
    pub session: SessionState,
}

#[derive(Debug, Default, Clone, FissionStateView)]
pub struct CartState {
    pub items: Vec<CartItem>,
}
Use GlobalState for durable product facts: current route, loaded records, form drafts that should survive navigation, selected account, save status, user preferences, and feature state shared by distant widgets.
Do not use GlobalState for runtime-owned details such as current animation interpolation values or host handle stores. Do not use it for tiny UI-local state that disappears with a component unless that state is product-significant.

Reading state during component conversion

Component conversion reads app inputs with fission::build::current::<S>():
let (ctx, view) = fission::build::current::<ShopState>();
S is the concrete GlobalState type the component expects. If a component does not read app state and only needs state-independent local wiring, it may request the common scope with ():
let (ctx, _) = fission::build::current::<()>();
build::current::<S>() is valid only inside an active build pass. Calling it outside component conversion panics with a build-scope error.

ViewHandle<S>

ViewHandle<S> is the read-side handle for component conversion.
Method
Purpose
state()
Returns the current &S app state
global()
Returns the generated typed state view when S: FissionViewField
select(...)
Runs a one-off closure selector against &S
select_with::<T>()
Runs a reusable Selector<S> implementation
env()
Reads the current environment
theme()
Reads env.theme
i18n()
Reads env.i18n
viewport_size()
Reads the current viewport size
layout()
Reads the optional previous layout snapshot
get_rect(id)
Reads a previous layout rectangle for a widget id
get_constraints(id)
Reads previous constraints for a widget id
animation_value(id, property)
Reads runtime animation state for a widget property
video_state(id)
Reads runtime video state for a widget
ViewHandle is not a long-lived reference. Store derived values, not the handle itself, if data must travel into an action or effect payload.

BuildCtxHandle<S>

BuildCtxHandle<S> is the write-side wiring handle for component conversion. It records relationships the runtime will use later.
Method
Purpose
bind(action, handler)
Creates an ActionEnvelope for a normal app-state reducer
register::<A, H>(handler)
Registers a reducer handler without immediately binding a widget event
bind_local(action, field, handler)
Binds a reducer to a retained local StateField<T>
request_animation_for(id, request)
Queues an animation request for a widget id
anim_for(id)
Creates a scoped animation helper for a target widget id
register_video(...)
Registers a video node with the runtime
register_web_view(...)
Registers a web view node with the runtime
register_portal(...)
Registers a default-layer portal
register_portal_with_id(...)
Registers a portal with an explicit widget id
register_portal_with_layer(...)
Registers a portal in a specific layer
with_resources(...)
Gives controlled access to the build resource registry
video_controls(id)
Creates controls for a registered video widget
BuildCtxHandle does not mutate app state directly. App state changes happen in reducers. The handle records that a future event should dispatch an action, update local state, start an animation, register a resource, or connect another runtime-managed behavior.

Handle lifetime

BuildCtxHandle and ViewHandle are copyable handles, not raw references. They resolve through the active build scope. This lets Fission expose ergonomic let (ctx, view) = build::current::<State>(); syntax without handing authoring code a reference that can outlive the shell-owned build data.
Correct usage:
impl From<CartBadge> for Widget {
    fn from(_: CartBadge) -> Widget {
        let (_, view) = fission::build::current::<ShopState>();
        Text::new(format!("{}", view.state().cart.items.len())).into()
    }
}
Incorrect usage:
// Do not store handles for later reducer, async, service, or static use.
struct BadCache<S: GlobalState> {
    view: ViewHandle<S>,
}
If a handle is used outside an active build pass, Fission panics with a clear scope error. That is part of the contract; it prevents stale build handles from becoming hidden global state.

Local widget state

Local widget state is retained state associated with a component field and widget identity.
Declare it with #[fission_component] and #[local_state(default = ...)]:
#[fission_component]
pub struct Counter {
    #[local_state(default = 0)]
    count: i32,
}
The macro removes local-state fields from the component's public prop fields and generates accessor methods. The accessor returns StateField<T>:
let count = counter.count();
let value = count.get();
Bind a local reducer with BuildCtxHandle::bind_local:
#[fission_reducer(Increment)]
fn increment(count: &mut i32) {
    *count += 1;
}

let (ctx, _) = fission::build::current::<()>();
let count = counter.count();
let on_press = ctx.bind_local(Increment, count.clone(), reduce!(increment));
Local state persists across rebuilds while the same widget identity is active. It is pruned after a build when that identity is no longer present. It is not persisted across app restarts unless the app explicitly mirrors it into durable state.

StateField<T>

StateField<T> is the handle returned by local-state accessors.
Method
Purpose
get()
Clones and returns the current retained value
component()
Returns the component identity string used by the generated accessor
field()
Returns the field name
key()
Returns the LocalStateKey
action_id::<A>()
Builds the scoped local action id for action A
StateField<T> is intentionally a handle. Updating it happens through ctx.bind_local(...) and a reducer, not by mutating the field directly inside component conversion.

WidgetId and local state identity

Fission can distinguish simple static siblings by structural position. Dynamic children that can be inserted, removed, filtered, or reordered should use explicit widget ids.
let children = rows
    .into_iter()
    .map(|row| RowCard { row: row.clone() }
        .id(WidgetId::explicit(&format!("row.{}", row.id))))
    .collect();
Use a durable key from the data. Do not use the list index for reorderable data. An unstable id makes retained local state appear to move between rows.

Provider stack

Provider<T, F> installs a typed value for descendants during component conversion.
#[derive(Clone)]
struct SectionName(&'static str);

Provider::new(SectionName("billing"), || BillingFields).into()
A descendant reads the nearest active value of that type:
let section = fission::build::read::<SectionName>();
Use try_read::<T>() when the value is optional:
let section = fission::build::try_read::<SectionName>()
    .unwrap_or(SectionName("default"));
Provider values must be Clone + Send + Sync + 'static. Reads clone the value. This avoids borrowed stack data escaping the build pass.
Providers are nested. The nearest provider of the requested type wins; when a nested provider finishes converting its child, the parent value becomes visible again.
Providers are not durable state. Use them for read-only contextual values scoped to a subtree. Use GlobalState or local widget state for values that need reducers or retention.

Action

An Action is the typed message that describes user intent or a runtime result.
The trait requires values to be serializable and to provide a stable ActionId through static_id(). In ordinary app code, prefer #[fission_reducer(ActionName)] when one reducer owns one action. Use #[fission_action] for manual action structs that are shared, public, or need separately documented fields.
Good action names describe intent in product language: SaveRequested, FilterChanged, CardAddedToCart, SessionExpired, or PhotoCaptured.
Avoid vague actions such as Clicked or Update unless the surrounding type makes the intent unambiguous.

Reducers

A reducer receives current state, an action, and optionally ReducerContext, then updates state.
Typical shapes:
#[fission_reducer(SetFilter)]
fn on_set_filter(state: &mut ShopState, filter: ProductFilter) {
    state.filter = filter;
}

#[fission_reducer(SaveRequested)]
fn on_save_requested(state: &mut ShopState, ctx: &mut ReducerContext<ShopState>) {
    state.saving = true;
    // request outside work through ctx.effects
}
Reducers are the normal place for state mutation. They should be deterministic except for explicitly requested effects. Do not put host work, network calls, file writes, or hidden global mutation directly inside a reducer body.

ReducerContext<S>

ReducerContext<S> is available only while a reducer is running.
Field
Purpose
effects
Queues jobs, services, commands, capabilities, timers, and other runtime-managed work
input
Reads explicit input that arrived with this dispatch, such as job results or capability payloads
ReducerContext is not storage. It should not be retained after the reducer returns.

Selectors and generated views

Generated state views give component conversion a typed path through state:
let (_, view) = fission::build::current::<ShopState>();
let cart_count = view.global().cart().items().map(|items| items.len()).get();
Use #[derive(FissionStateView)] on nested state structs you want to traverse this way. Scalar and collection fields return ValueView<T>, which borrows the source field and clones only when get() is called.
For one-off reads, use view.select(...):
let cart_count = view.select(|state| state.cart.items.len());
For reusable derived view models, implement Selector<S> and call view.select_with::<YourSelector>().
Selectors are read-only. Do not use them to mutate state or hide side effects.

Decision table

Need
API
Durable product data
GlobalState
One widget's temporary retained memory
#[local_state] and StateField<T>
Parent-scoped contextual read-only data
Provider<T, F>, read::<T>(), try_read::<T>()
Theme, locale, viewport, window, safe-area, and platform presentation context
Env through ViewHandle
Bind a widget event to app-state change
BuildCtxHandle::bind(...) or with_reducer!(...)
Bind a widget event to local state change
BuildCtxHandle::bind_local(...)
Read app state or derived view models
ViewHandle::state, global, select, or select_with
Request outside work
ReducerContext::effects
Keep identity stable across reordered dynamic children
.id(WidgetId::explicit(...))
Read State, handles, and providers for the guided version of this reference. For async work and reducer-side effect emission, continue to Commands, services, and jobs and Resources and capabilities. For the component conversion boundary that reads from this state system, see Widget authoring boundary.
Fission
A cross-platform, GPU-accelerated user interface framework for Rust. MIT licensed.
Copyright (c) 2026 Fission
Ready to use today. Widget APIs are expected to remain stable; some runtime and shell APIs may change before 1.0.0.
Fission 0.7.0