Expand description
A lightweight Model-View-Update (MVU) runtime for Rust with no_std support.
Implements the MVU pattern for building predictable, testable applications with unidirectional data flow and controlled side effects.
§Overview
The Model-View-Update (MVU) pattern, also known as the Elm Architecture, structures applications as a pure functional loop with three main components:
- Model: Immutable state representing your entire application
- Update: Pure function that transforms
(Event, Model) → (Model, Effect) - View: Pure function that derives renderable Props from the Model
This architecture makes state transitions predictable, debuggable, and testable by eliminating implicit state mutation and enforcing unidirectional data flow.
§Core Concepts
§Model
The Model is an immutable data structure representing your application’s complete state. All behavior is a pure function of this state - nothing happens outside of it.
#[derive(Clone)]
struct Model {
counter: i32,
user_name: String,
is_loading: bool,
}§Event
Events are discrete, immutable messages that trigger state transitions. They represent inputs
from users, systems, or asynchronous effects. Events must implement the Clone trait.
#[derive(Clone)]
enum Event {
UserClickedButton,
DataLoaded(String),
TimerTicked,
}§Effect
Effects declaratively describe side-effecting work (async I/O, timers, etc.) that produces events. They maintain purity in your update logic while enabling real-world interactions.
Depending on your spawner implementation, effects may be processed in parallel.
// Describe an async operation that will emit an event
let effect = Effect::from_async(async move |emitter| {
let data = fetch_data().await;
emitter.emit(Event::DataLoaded(data)).await;
});§Props
Props are a pure, derived projection of the Model optimized for rendering or external
presentation. They describe WHAT to render without prescribing HOW. Props commonly
contain data and callbacks created via Emitter.
Props are not limited to UI - they can represent any external projection (API responses, hardware states, serialized output).
§Architecture
The MVU runtime orchestrates a unidirectional event loop:
┌──────────────────────────────────────────────────────┐
│ │
│ User Input / External Signal ◀──────────┐ │
│ (ie. Props callback or effect) │ │
│ │ │ │
│ ▼ │ │
│ ┌────────┐ │ │
│ │ Event │ │ │
│ └────┬───┘ │ │
│ │ // sequenced by │ │
│ ▼ // the runtime │ │
│ ┌───────────────┐ │ │
│ │ update(event, │ ┌───────┴─────┐ │
│ │ model) │ │ emit(event) │ │
│ └───────┬───────┘ └─────────────┘ │
│ │ ▲ │
│ ▼ │ │
│ ┌─────────────────────┐ │ │
│ │ (Model, Effect) │ │ │
│ └──────┬──────┬───────┘ │ │
│ │ │ │ │
│ │ └──► Effect task spawn ───┘ │
│ │ ▲ │
│ ▼ │ │
│ ┌─────────┐ │ │
│ │ Model │ │ │
│ └────┬────┘ │ │
│ │ │ │
│ ▼ │ │
│ ┌──────────────┐ │ │
│ │ view(model, │ ┌────────┴───────┐ │
│ │ emitter) │ │ props callback │ │
│ └──────┬───────┘ │ uses emitter │ │
│ │ └────────────────┘ │
│ ▼ ▲ │
│ ┌───────┐ │ │
│ │ Props │ // emitter captured │ │
│ └───┬───┘ // in Props callbacks │ │
│ │ │ │
│ ▼ │ │
│ ┌───────────────┐ ┌────────────┐ │
│ │ render(props) │ ─────────────► │ user input │ │
│ └───────────────┘ └────────────┘ │
│ │
└──────────────────────────────────────────────────────┘§When to Use
MVU is ideal for:
- Applications requiring predictable, debuggable state management
- Event-driven systems (GUIs, embedded controllers, game loops)
- Applications where state can be serialized/replayed (time-travel debugging)
- Teams prioritizing testability and clear separation of concerns
no_stdenvironments that can afford minimal heap allocations and meet prior criteria
Consider alternatives if:
- You need direct object mutation for performance-critical inner loops
- Your application is primarily synchronous with minimal state
§Platform Support
§Standard Environments
By default, oxide-mvu works with the standard library.
[dependencies]
oxide-mvu = "0.4.2"§no_std Environments
For embedded systems or environments without the standard library, enable the
no_std feature. This requires an allocator (alloc crate) but replaces all
standard library dependencies with no_std crates:
[dependencies]
oxide-mvu = { version = "0.4.2", features = ["no_std"] }The runtime uses lock-free concurrency and bounded channels to minimize the cost of event synchronization. This makes the framework concurrency-model agnostic. Effects may execute in parallel or concurrently on the same thread as the runtime depending on hardware availability and your spawner implementation.
§Quick Start
use oxide_mvu::{Emitter, Effect, MvuLogic, MvuRuntime, Renderer};
#[derive(Clone)]
enum Event {
AccumulateClicked,
}
#[derive(Clone)]
struct Model {
count: i32,
}
struct Props {
count: i32,
on_accumulate_click: Box<dyn Fn()>,
}
struct MyLogic;
impl MvuLogic<Event, Model, Props> for MyLogic {
fn init(&self, model: Model) -> (Model, Effect<Event>) {
(model, Effect::none())
}
fn update(&self, event: Event, model: &Model) -> (Model, Effect<Event>) {
match event {
Event::AccumulateClicked => {
let new_model = Model {
count: model.count + 1,
..model.clone()
};
(new_model, Effect::none())
}
}
}
fn view(&self, model: &Model, emitter: &Emitter<Event>) -> Props {
let emitter = emitter.clone();
Props {
count: model.count,
on_accumulate_click: Box::new(move || {
emitter.try_emit(Event::AccumulateClicked);
}),
}
}
}
struct MyRenderer;
impl Renderer<Props> for MyRenderer {
fn render(&mut self, _props: Props) {}
}
async fn main_async() {
// Create a spawner for your async runtime.
// This is how `Effect`s are executed.
let spawner = |fut| {
// Spawn the future on your chosen runtime.
// Examples:
// tokio::spawn(fut);
// async_std::task::spawn(fut);
let _ = fut;
};
let runtime = MvuRuntime::builder(
Model { count: 0 },
MyLogic,
MyRenderer,
spawner,
).build();
// `run()` returns a Future representing the event loop.
// It must be awaited inside an async context.
runtime.run().await;
}In a real application, main_async would be executed by your async runtime
(e.g. via #[tokio::main], async_std::main, or an embedded executor).
§Advanced Topics
§Effect Composition
Multiple effects can be combined using Effect::batch:
let effect = Effect::batch(vec![
Effect::just(Event::A),
Effect::just(Event::B),
Effect::just(Event::C),
]);§Event Buffer Capacity
The runtime uses a bounded channel to queue events. The default capacity
(DEFAULT_EVENT_CAPACITY = 32) is sized for embedded systems with limited heap.
Customize this via the builder:
// Memory-constrained embedded systems
let runtime = MvuRuntime::builder(Model, Logic, MyRenderer, spawner)
.capacity(8)
.build();
// High-throughput applications with event bursts
let runtime = MvuRuntime::builder(Model, Logic, MyRenderer, spawner)
.capacity(1024)
.build();When the buffer is full:
Emitter::try_emitreturnsfalseand drops the eventEmitter::emitawaits until space is available (backpressure)
§Testing
Enable the testing feature to access test utilities:
[dev-dependencies]
oxide-mvu = { version = "0.4.2", features = ["testing"] }Test your MVU logic deterministically:
use oxide_mvu::{Effect, MvuLogic, Renderer, TestMvuRuntime, create_test_spawner};
let runtime = TestMvuRuntime::builder(
Model { count: 0 },
Logic,
MyRenderer,
create_test_spawner()
).build();
let mut driver = runtime.run();
// Manually process events in tests
driver.process_events();See TestMvuRuntime (available with the testing feature) for comprehensive testing utilities.
§Async Runtime Integration
The Spawner trait abstracts over different async runtimes. Common patterns:
// tokio
let spawner = |fut| { tokio::spawn(fut); };
// async-std
let spawner = |fut| { async_std::task::spawn(fut); };
// smol
let spawner = |fut| { smol::spawn(fut).detach(); };§See Also
MvuLogic- The core trait defining application behaviorEffect- Declarative side effect systemEmitter- Event dispatch from Props callbacksRenderer- Integration point for rendering systemsMvuRuntime- The runtime orchestrating the event loop
Structs§
- Emitter
- Event emitter that can be embedded in Props.
- MvuRuntime
- The MVU runtime that orchestrates the event loop.
- MvuRuntime
Builder - Builder for configuring and constructing an
MvuRuntime.
Enums§
- Effect
- Declarative description of events to be processed.
Constants§
- DEFAULT_
EVENT_ CAPACITY - Default event channel capacity.