Fission for Tauri developers

Tauri and Fission both appeal to developers who want Rust in desktop/mobile app architecture. Tauri keeps a web frontend plus Rust commands. Fission makes the UI model itself Rust-first and shared across more target shells.
The main shift is that Fission does not treat Rust as only the backend behind a webview. State, widgets, reducers, routing, theme, and tests are all part of the shared Rust app model.
This page uses the same todo app shape as the React guide: a checklist with a draft field, add/toggle actions, loading from an outside source, token-driven styling, routing, and tests.

The state ownership rule

Before translating syntax, decide where state belongs:
Use #[fission_component] and #[local_state] for retained UI memory owned by one widget identity.
Use GlobalState for product truth: todos, routing, persistence, sync state, user preferences, and shared filters.
Use reducers for named user intent and state transitions.
Use jobs, services, resources, capabilities, or runtime effects for outside work.
Add semantic identifiers to interactive widgets and meaningful regions so tests and accessibility tools can target product meaning instead of coordinates.

1. Static UI

{"id":"fission-tabs-1","tabs":[{"index":0,"label":"Tauri","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-1","index":0,"label":"Tauri","value":"source"}
  return (
    <main>
      <h1>Ship checklist</h1>
      <TodoRow title="Write release notes" done={false} />
      <TodoRow title="Test Android package" done />
    </main>
  );
}
{"id":"fission-tabs-1","index":1,"label":"Fission","value":"fission"}
use fission::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Todo {
    pub id: u64,
    pub title: String,
    pub done: bool,
}

pub struct TodoApp;

impl From<TodoApp> for Widget {
    fn from(_app: TodoApp) -> Widget {
        let todos = vec![
            Todo { id: 1, title: "Write release notes".into(), done: false },
            Todo { id: 2, title: "Test Android package".into(), done: true },
        ];

        let rows = todos.into_iter().map(|todo| {
            let status = if todo.done { "Done" } else { "Open" };
            Row {
                gap: Some(12.0),
                children: widgets![Text::new(status), Text::new(todo.title)],
                ..Default::default()
            }
            .into()
        });

        Container::new(Column {
            gap: Some(12.0),
            children: std::iter::once(Text::new("Ship checklist").size(28.0).weight(800).into())
                .chain(rows)
                .collect(),
            ..Default::default()
        })
        .padding_all(24.0)
        .into()
    }
}
{"id":"fission-tabs-1"}
The Fission side is a concrete widget value. Conversion reads inputs and returns a widget tree; it is not the place to start outside work or hide product state.

2. Product state and interaction

{"id":"fission-tabs-2","tabs":[{"index":0,"label":"Tauri","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-2","index":0,"label":"Tauri","value":"source"}
  const [draft, setDraft] = useState("");
  const [todos, setTodos] = useState<Todo[]>([]);

  async function addDraftTodo() {
    const todo = await invoke<Todo>("add_todo", { title: draft });
    setTodos((items) => [...items, todo]);
    setDraft("");
  }
}
{"id":"fission-tabs-2","index":1,"label":"Fission","value":"fission"}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TodoState {
    pub draft: String,
    pub todos: Vec<Todo>,
    pub next_id: u64,
    pub current_path: String,
}

impl Default for TodoState {
    fn default() -> Self {
        Self {
            draft: String::new(),
            todos: Vec::new(),
            next_id: 1,
            current_path: "/".into(),
        }
    }
}

impl GlobalState for TodoState {}

#[fission_reducer(SetDraft)]
fn set_draft(state: &mut TodoState, draft: String) {
    state.draft = draft;
}

#[fission_reducer(AddDraftTodo)]
fn add_draft_todo(state: &mut TodoState) {
    let title = state.draft.trim();
    if title.is_empty() {
        return;
    }

    state.todos.push(Todo {
        id: state.next_id,
        title: title.to_string(),
        done: false,
    });
    state.next_id += 1;
    state.draft.clear();
}

#[fission_reducer(ToggleTodo)]
fn toggle_todo(state: &mut TodoState, id: u64) {
    if let Some(todo) = state.todos.iter_mut().find(|todo| todo.id == id) {
        todo.done = !todo.done;
    }
}
pub struct TodoScreen;

impl From<TodoScreen> for Widget {
    fn from(_screen: TodoScreen) -> Widget {
        let (ctx, view) = fission::build::current::<TodoState>();
        let update_draft = with_reducer!(ctx, SetDraft(String::new()), set_draft);
        let add_todo = with_reducer!(ctx, AddDraftTodo, add_draft_todo);

        let rows = view.state().todos.iter().map(|todo| {
            let toggle = with_reducer!(ctx, ToggleTodo(todo.id), toggle_todo);
            Row {
                gap: Some(12.0),
                children: widgets![
                    Checkbox {
                        checked: todo.done,
                        on_toggle: Some(toggle),
                        ..Default::default()
                    }
                    .semantics_identifier(format!("todo.toggle.{}", todo.id)),
                    Text::new(todo.title.clone()),
                ],
                ..Default::default()
            }
            .semantics_identifier(format!("todo.row.{}", todo.id))
            .into()
        });

        Column {
            gap: Some(16.0),
            children: widgets![
                Text::new("Ship checklist").size(28.0).weight(800),
                Row {
                    gap: Some(12.0),
                    children: widgets![
                        TextInput {
                            id: Some(WidgetId::explicit("todo-title")),
                            value: view.state().draft.clone(),
                            label: Some("New task".into()),
                            placeholder: Some("Add a task".into()),
                            on_change: Some(update_draft),
                            ..Default::default()
                        }
                        .semantics_identifier("todo.title"),
                        Button {
                            child: Some(Text::new("Add task").into()),
                            on_press: Some(add_todo),
                            ..Default::default()
                        }
                        .semantics_identifier("todo.add"),
                    ],
                    ..Default::default()
                },
                Column { gap: Some(10.0), children: rows.collect(), ..Default::default() },
            ],
            ..Default::default()
        }
        .into()
    }
}
{"id":"fission-tabs-2"}
Fission makes the state transition explicit. The button does not mutate a captured local variable; it dispatches a named action and the reducer updates product state.

3. Outside work

{"id":"fission-tabs-3","tabs":[{"index":0,"label":"Tauri","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-3","index":0,"label":"Tauri","value":"source"}
#[tauri::command]
async fn load_todos(state: tauri::State<'_, TodoStore>) -> Result<Vec<Todo>, String> {
    state.load().await.map_err(|err| err.to_string())
}

const todos = await invoke<Todo[]>("load_todos");
{"id":"fission-tabs-3","index":1,"label":"Fission","value":"fission"}
use fission::core::{JobRef, JobSpec};
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LoadTodosRequest;

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ApiError {
    pub message: String,
}

pub struct LoadTodosJob;

impl JobSpec for LoadTodosJob {
    type Request = LoadTodosRequest;
    type Ok = Vec<Todo>;
    type Err = ApiError;

    const NAME: &'static str = "todo.load";
}

pub const LOAD_TODOS_JOB: JobRef<LoadTodosJob> = JobRef::new(LoadTodosJob::NAME);
#[fission_reducer(LoadTodos)]
fn load_todos(state: &mut TodoState, ctx: &mut ReducerContext<TodoState>) {
    state.loading = true;
    ctx.effects
        .app(LOAD_TODOS_JOB, LoadTodosRequest)
        .on_ok(ctx.effects.bind(TodosLoaded, reduce_with!(todos_loaded)))
        .on_err(ctx.effects.bind(TodosFailed, reduce_with!(todos_failed)));
}
{"id":"fission-tabs-3"}
Do not perform outside work during component conversion. Reducers request work; jobs, services, resources, or capabilities perform it; results return as actions.

4. Styling and theme

{"id":"fission-tabs-4","tabs":[{"index":0,"label":"Tauri","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-4","index":0,"label":"Tauri","value":"source"}
<button className="rounded-xl border bg-white px-4 py-3 shadow-sm">
  <input type="checkbox" checked={todo.done} readOnly />
  <span>{todo.title}</span>
</button>
{"id":"fission-tabs-4","index":1,"label":"Fission","value":"fission"}
pub struct TodoRow {
    pub todo: Todo,
}

impl From<TodoRow> for Widget {
    fn from(row: TodoRow) -> Widget {
        let (ctx, view) = fission::build::current::<TodoState>();
        let tokens = &view.env().theme.tokens;
        let toggle = with_reducer!(ctx, ToggleTodo(row.todo.id), toggle_todo);

        Container::new(Row {
            gap: Some(tokens.spacing.s),
            children: widgets![
                Checkbox {
                    checked: row.todo.done,
                    on_toggle: Some(toggle),
                    ..Default::default()
                }
                .semantics_identifier(format!("todo.toggle.{}", row.todo.id)),
                Text::new(row.todo.title)
                    .size(tokens.typography.font_size_base)
                    .color(tokens.colors.text_primary),
            ],
            ..Default::default()
        })
        .bg(tokens.colors.surface)
        .border(tokens.colors.border, 1.0)
        .border_radius(tokens.radii.large)
        .padding([tokens.spacing.m, tokens.spacing.m, tokens.spacing.s, tokens.spacing.s])
        .into()
    }
}
{"id":"fission-tabs-4"}
In Fission, the active theme is runtime environment. Read token values from view.env().theme.tokens and apply them through widget fields. That keeps the UI target-lowerable across native, web, mobile, terminal, static, and SSR shells.

5. Routing

{"id":"fission-tabs-5","tabs":[{"index":0,"label":"Tauri","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-5","index":0,"label":"Tauri","value":"source"}
<BrowserRouter>
  <Routes>
    <Route path="/" element={<TodoApp />} />
    <Route path="/todos/:id" element={<TodoDetail />} />
  </Routes>
</BrowserRouter>
{"id":"fission-tabs-5","index":1,"label":"Fission","value":"fission"}
use fission::widgets::{Route, Router};
use std::sync::Arc;

#[fission_reducer(NavigateTo)]
fn navigate_to(state: &mut TodoState, path: String) {
    state.current_path = path;
}

pub struct TodoRoutes;

impl From<TodoRoutes> for Widget {
    fn from(_routes: TodoRoutes) -> Widget {
        let (_, view) = fission::build::current::<TodoState>();

        Router::<TodoState> {
            current_path: view.state().current_path.clone(),
            routes: vec![
                Route {
                    path: "/".into(),
                    builder: Arc::new(|_, _, _| TodoScreen.into()),
                },
                Route {
                    path: "/todos/:id".into(),
                    builder: Arc::new(|_, _, params| TodoDetail {
                        id: params["id"].parse().unwrap_or(0),
                    }
                    .into()),
                },
            ],
            not_found: Some(Arc::new(|_, _, _| Text::new("Not found").into())),
        }
        .into()
    }
}
{"id":"fission-tabs-5"}
Use Fission's native Router and RouterParams. Shell URLs, deep links, static routes, server routes, and terminal route-like state should normalize into the same app-owned route state instead of each target inventing a different navigation model.

6. Testing

{"id":"fission-tabs-6","tabs":[{"index":0,"label":"Tauri","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-6","index":0,"label":"Tauri","value":"source"}
await page.getByLabel('New task').fill('Publish release');
await page.getByRole('button', { name: 'Add task' }).click();
await expect(page.getByText('Publish release')).toBeVisible();
{"id":"fission-tabs-6","index":1,"label":"Fission","value":"fission"}
use fission_test_driver::{LiveTestClient, SelectorQuery};

#[test]
#[ignore]
fn user_can_add_a_todo_in_a_real_shell() {
    let control_port = reserve_control_port();
    let mut child = launch_todo_app(control_port);
    let client = LiveTestClient::connect(control_port);

    client.wait_for_ready(15_000).expect("app ready");
    client
        .fill_text_semantic_identifier("todo.title", "Publish release")
        .expect("fill draft");
    client
        .tap_semantic_identifier("todo.add")
        .expect("add todo");
    client
        .wait_for_visible(SelectorQuery::label("Publish release"), 5_000)
        .expect("todo visible");
    client
        .screenshot(".artifacts/screenshots/todos/added.png")
        .expect("screenshot");

    client.quit().expect("quit app");
    let _ = child.wait();
}
{"id":"fission-tabs-6"}
Keep fast reducer tests for business rules. Use headless fission-test for widget/layout/semantics assertions. Use LiveTestClient and /cmd for real shell input, focus, screenshots, selectors, and target behavior.

Vocabulary map

Tauri concept
Closest Fission concept
Important difference
Web frontend
Fission widget tree
The UI is Rust-native, not a webview app model.
#[tauri::command]
Job, service, resource, or capability
Calls are typed runtime work requested by reducers.
Plugin
Capability/provider/native module
Host integration is explicit and target-aware.
tauri.conf.json bundle config
fission.toml targets/package config
Package artifacts produce manifest handoffs.
Web tests
LiveTestClient and /cmd
Tests target semantics rather than DOM selectors.

Common instincts to adjust

Do not split product state between a frontend store and Rust command state unless there is a real ownership reason. Fission product state should be one visible app model.
Do not turn every host operation into a generic stringly IPC call. Prefer typed jobs, services, capabilities, and reducers.
Keep the Tauri instinct that packaging, signing, and native capabilities are first-class app concerns.
Read Testing Fission apps for unit, headless integration, LiveTest, and /cmd workflows.
Read State, handles, and providers for the durable-state, local-state, provider, and environment decision model.
Read Resources and async for jobs, services, resources, capabilities, and reducer effects.
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.9.0