Build a counter

A counter is a good first Fission recipe because it is small, interactive, and does not need app-wide domain state. The count is local UI state owned by this widget identity, so it disappears when the counter is removed and does not have to live in GlobalState.

Full example

use fission::prelude::*;

#[fission_component]
struct CounterApp {
    #[local_state(default = 0)]
    count: i32,
}

#[fission_reducer(Increment)]
fn increment(count: &mut i32) {
    *count += 1;
}

impl From<CounterApp> for Widget {
    fn from(counter: CounterApp) -> Widget {
        let (ctx, _) = fission::build::current::<()>();
        let count = counter.count();
        let increment = ctx.bind_local(Increment, count.clone(), reduce!(increment));

        Container::new(Column {
            gap: Some(20.0),
            children: widgets![
                Text::new("Counter").size(32.0),
                Text::new(format!("{}", count.get())).size(56.0),
                Button {
                    on_press: Some(increment),
                    child: Some(Text::new("Increment").into()),
                    ..Default::default()
                },
            ],
            ..Default::default()
        })
        .padding_all(32.0)
        .into()
    }
}

fn main() -> anyhow::Result<()> {
    DesktopApp::<(), _>::new(CounterApp {}).run()
}

What matters

#[fission_component] marks CounterApp as a component that can own retained local fields.
#[local_state(default = 0)] makes count local widget state rather than app-wide GlobalState.
build::current::<()>() asks for the build handles without requiring an app state type.
ctx.bind_local(...) wires the generated Increment action to the local count field.
impl From<CounterApp> for Widget is the public authoring boundary: the component converts into the closed Widget tree.
Use GlobalState when the value is part of your app model, persistence, routing, sync, or shared behavior. Use local widget state for temporary UI state like this counter, a local draft value, an open/closed flag, or a selected tab inside one component. The longer guide to this decision is State, handles, and providers.

Finished state and checks

By the end of Build a counter, you should have a runnable change, not just a set of snippets. Run the command shown in the page, exercise the feature once manually, and add or update the smallest test that protects the behavior.
Check
Expected result
Build
cargo check or the relevant fission command finishes without target-specific configuration errors.
Behavior
The screen, package, server route, or test flow described by the recipe is visible and responds to input.
Failure path
At least one denied, missing, invalid, or empty case renders a useful message instead of silently doing nothing.
Next link
The reference page linked from the recipe explains the exact fields, commands, or types used here.
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