0Pricing
Learn Rust Coding · Lesson

Managing App State

Hold and update UI data.

Managing App State is a free Learn Rust Coding lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Learn Rust Coding learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

State on the App Struct

In egui your application state is plain Rust data living on the struct that implements eframe::App. There is no special store or framework-owned model.

Fields hold everything: form inputs, selections, loaded data, flags. Each frame, update reads and mutates these fields directly through &mut self.

struct App {
    query: String,
    results: Vec<Item>,
    selected: Option<usize>,
    loading: bool,
}

Deriving Default

eframe builds your app in a closure passed to run_native. The simplest pattern is #[derive(Default)] and constructing with App::default().

For non-default initial values, implement Default by hand or add a constructor that takes the CreationContext for setup like fonts or persistence.

impl App {
    fn new(cc: &eframe::CreationContext) -> Self {
        let mut app = Self::default();
        app.dark = cc.egui_ctx.style().visuals.dark_mode;
        app
    }
}

Single Source of Truth

Because widgets borrow &mut to their backing fields, the struct is always the single source of truth. There is no separate view state to keep in sync.

A checkbox bound to &mut self.enabled reflects and updates that bool directly — read it anywhere else in the same frame and you see the live value.

ui.checkbox(&mut self.enabled, "Enabled");
if self.enabled {
    ui.label("Feature is on");
}

Deferring Mutations

A subtle borrow issue: you often iterate a Vec while wanting to remove from it. You cannot mutate the vec while a shared borrow is alive.

The fix is to record the intended action in a local variable during the loop, then apply it after the loop ends.

let mut to_remove = None;
for (i, item) in self.items.iter().enumerate() {
    ui.horizontal(|ui| {
        ui.label(&item.name);
        if ui.button("x").clicked() { to_remove = Some(i); }
    });
}
if let Some(i) = to_remove { self.items.remove(i); }

Enum-Driven Screens

For multi-screen apps, model the current view as an enum field. The update method matches on it to decide what to draw.

This keeps navigation explicit and exhaustive — the compiler forces you to handle every screen, eliminating whole classes of routing bugs.

enum Screen { Home, Settings, About }

match self.screen {
    Screen::Home => self.draw_home(ui),
    Screen::Settings => self.draw_settings(ui),
    Screen::About => self.draw_about(ui),
}

egui Memory for UI State

Some state is purely about the UI, not your domain — collapsed sections, scroll positions, drag state. egui stores this in ctx.memory, keyed by widget Id.

You rarely touch it directly, but ctx.data_mut lets you stash small per-id values that persist between frames without cluttering your app struct.

let id = egui::Id::new("my_toggle");
let mut open = ctx.data_mut(|d| d.get_temp::<bool>(id).unwrap_or(false));
ui.checkbox(&mut open, "Open");
ctx.data_mut(|d| d.insert_temp(id, open));

Background Work with Channels

Never block update with slow I/O — it freezes the 60fps loop. Spawn a thread and communicate via an std::sync::mpsc channel.

The worker sends results back; update drains the receiver non-blocking with try_recv each frame. Call ctx.request_repaint() from the worker so the UI wakes when data arrives.

if let Ok(msg) = self.rx.try_recv() {
    self.results = msg;
    self.loading = false;
}

Spawning the Worker

Clone the Context and the sender into the thread. The clone is cheap — it is an Arc internally — and lets the worker request repaints.

Move ownership of the work inputs into the closure to satisfy the borrow checker and avoid lifetime issues.

let tx = self.tx.clone();
let ctx = ctx.clone();
std::thread::spawn(move || {
    let data = expensive_load();
    let _ = tx.send(data);
    ctx.request_repaint();
});

Persisting State

eframe can serialize your app between runs. Enable the persistence feature, derive serde::Serialize / Deserialize, and implement App::save.

On startup, read the saved state from cc.storage in your constructor, falling back to defaults when nothing is stored.

fn save(&mut self, storage: &mut dyn eframe::Storage) {
    eframe::set_value(storage, eframe::APP_KEY, self);
}
// in new():
if let Some(s) = cc.storage {
    if let Some(app) = eframe::get_value(s, eframe::APP_KEY) { return app; }
}

Keeping update Cheap

Since update runs every frame, avoid recomputing expensive derived data there. Cache results in fields and recompute only when inputs change.

A common pattern: track a dirty flag set by Response::changed(), and rebuild caches at the top of update only when dirty.

if self.dirty {
    self.filtered = self.filter_items();
    self.dirty = false;
}
for item in &self.filtered { ui.label(&item.name); }

Ownership Keeps It Honest

The recurring theme: egui leans on Rust ownership instead of frameworks for state management. Your struct owns the data, &mut self grants exclusive access per frame, and threads communicate through channels.

There are no hidden subscriptions or callback graphs, so reasoning about when and how state changes stays local and explicit.

Quick Check

How should you run a slow network fetch?

Recap

App state is plain fields on the eframe::App struct, mutated through &mut self each frame, making the struct the single source of truth.

Defer mutations during iteration, model screens with enums, offload slow work to threads with channels, and persist with eframe's storage. Keep update cheap by caching derived data behind dirty flags. Next: packaging your app for distribution.

Frequently asked questions

Is the “Managing App State” lesson free?

Yes — the full text of “Managing App State” is free to read here on the web, and the Learn Rust Coding course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Learn Rust Coding course, upgrade to CoddyKit PRO.

What will I learn in “Managing App State”?

Hold and update UI data. You practise Learn Rust Coding with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Learn Rust Coding?

No prior experience is required. Learn Rust Coding on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Managing App State” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Learn Rust Coding lesson?

Yes. Every Learn Rust Coding lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Immediate-Mode UI Basics
  2. Widgets and Layout
  3. Managing App State
  4. Packaging a Desktop App
← Back to Learn Rust Coding