The G is intentional. GLoc started as a hobby project called Godwin's Business Logic Component,
born from a mission to bring Flutter's legendary BLoC architecture into Rust.
But as it grows to serve the wider open-source community, that G now stands for Global.
One pattern. Universal. Everywhere Rust runs.
A universal business logic architecture for Rust.
GLoC is inspired by Flutter's Bloc architecture — but it's its own thing. It separates business logic from presentation in any Rust application and works anywhere Rust runs: web frontends, desktop GUIs, backend servers, CLIs, and embedded targets.
The core abstraction is Reactor — a single unit that owns one slice of domain state
and exposes domain methods that transition it. Unlike Flutter Bloc which has separate
Cubit and Bloc types, GLoC has one: a Reactor supports both direct method calls
and event dispatch.
┌─────────────────────────────────────────────────────────────┐
│ Without GLoC │ With GLoC │
│─────────────────────────│───────────────────────────────────│
│ Logic tangled in UI │ Reactor owns logic │
│ State scattered │ Single source of truth │
│ Hard to test │ Fully injectable & mockable │
│ Framework-locked │ Web · Desktop · CLI · Embedded │
└─────────────────────────────────────────────────────────────┘
One pattern. Everywhere Rust runs.
- Concepts
- Installation
- The story
- Quick Start
- Define State
- Define a Reactor
- Observers
- Reactive Layer
- Dioxus Example
- Feature Flags
- Contributing
- License
A Reactor is the central unit of business logic in GLoC — the equivalent of a BLoC in Flutter or a ViewModel in other architectures. It owns the current State, exposes methods to mutate it, and emits a new state to all subscribers whenever something changes.
You define a reactor as a plain Rust struct annotated with #[reactor]. GLoC generates the reactive plumbing — subscription, change detection, and provider wiring — so you only write the logic that matters.
#[reactor(state = CounterState)]
pub struct CounterReactor {}
impl CounterReactor {
pub fn increment(&mut self) {
self.emit(CounterState { count: self.count + 1 });
}
}A reactor that also accepts Neutrons receives them through a generated fire() method and routes them to your on_event handler, keeping dispatch decoupled from the call site.
A Neutron is an immutable event fired at a reactor — the signal that triggers a state transition. The name follows GLoC's nuclear fission theme: a neutron strikes the reactor core and causes a reaction.
Neutrons can be enums, structs, or any type that satisfies Debug + Send + 'static — no base trait to extend or import. Enums are the most common choice when a reactor handles multiple distinct events:
#[derive(Debug)]
pub enum CounterNeutron {
Increment,
Decrement,
Reset,
}
impl CounterReactor {
fn on_event(&mut self, neutron: CounterNeutron) {
match neutron {
CounterNeutron::Increment => self.emit(CounterState { count: self.count + 1 }),
CounterNeutron::Decrement => self.emit(CounterState { count: self.count - 1 }),
CounterNeutron::Reset => self.emit(CounterState { count: 0 }),
}
}
}
// At the call site:
reactor.fire(CounterNeutron::Increment);Neutrons are consumed on dispatch — not cloned or stored. Event is kept as a type alias for backward compatibility, but Neutron is the preferred term.
State is a snapshot of everything a reactor knows at a given moment — pure data, no behaviour. Any type that implements Clone + PartialEq + Debug is automatically a State. Use #[reactor_state] to skip writing the derives:
#[reactor_state]
pub struct CounterState {
pub count: i32,
}GLoC performs change detection: calling emit() with a value equal to the current state is a no-op — no notification is sent and no re-render is triggered. Only genuine transitions propagate.
State is always read through the reactor — directly via Deref (reactor.count) or through a subscription stream. Subscribers always receive the latest value and are notified on every real transition.
| Concept | Description |
|---|---|
emit() |
State-transition primitive inside a reactor. Built-in change detection — emitting the same value is a no-op. |
GlocStream |
Reactive state container — notifies listeners on every real transition. |
GlocProvider |
Shared Arc<Mutex<R>> handle for reading and mutating a reactor across threads or components. |
GlocListener |
Trait for typed old → new transition observers. |
GlocObserver |
Global observer that receives every transition across all reactors. |
Add a single dependency — gloc includes both the core traits and the #[reactor] macro:
[dependencies]
gloc = "0.2"Then import everything from one place:
use gloc::{reactor, Reactor, State, ReactorBase};Advanced — use the individual crates if you only need part of the library:
[dependencies]
gloc-core = "0.2" # traits only — Reactor, State, ReactorBase
gloc-macro = "0.2" # #[reactor] macro onlyWith tracing — logs every state transition via the tracing crate:
[dependencies]
gloc = { version = "0.2", features = ["tracing"] }
tracing = "0.1"GLoC began as a hobby experiment — Godwin's Logic Component — by a Flutter developer who jumped into Rust and immediately hit a wall. Coming from Flutter, BLoC and Cubit weren't just patterns — they were second nature. A clean boundary between logic and UI, testable in isolation, portable across any host. But in Rust? Nothing came close. Every framework had its own way of doing things, and the business logic you actually cared about kept getting buried under framework glue that should never have touched it in the first place. So instead of waiting, GLoC was born. Build a Dioxus desktop app. Port the same logic to an Axum backend. Drop it into a Bevy game. With GLoC, the logic stays exactly as it was — no rewrites, no adapters, no compromise. Not just UI frameworks. Any Rust application, anywhere Rust runs.
The Reactor is the core idea. A plain Rust struct that owns one slice of domain state and exposes methods to transition it. Zero framework imports. Zero async runtime. Zero signals. Write it once; it runs unchanged everywhere:
| Where | How you wire it |
|---|---|
| Unit test | CounterReactor::new(...) — call it directly |
| Dioxus desktop | `use_gloc_provide( |
| Axum backend | AxumReactor<CounterReactor> as Axum state |
| Bevy game | GlocPlugin::<CounterReactor> as an ECS resource |
| CLI / threads | GlocProvider::new(...) — share across threads |
The reactor never changes. The framework does.
The ecosystem is expanding in every direction at once — desktop, web, embedded, games, backend — and each corner is reinventing state management from scratch. GLoC wants to end that fragmentation.
Not by being another framework. By being a shared vocabulary for state: one pattern that travels with you from a weekend project to a production app, from a solo experiment to a team codebase, from a CLI tool to a shipped product.
If you learn it once, your business logic is portable forever.
The common case. Create a reactor, call methods, listen to transitions.
use gloc::{reactor, reactor_state, Reactor};
#[reactor_state]
pub struct CounterState { pub count: i32 }
#[reactor(state = CounterState)]
pub struct CounterReactor {}
impl CounterReactor {
pub fn increment(&mut self) {
self.emit(CounterState { count: self.state().count + 1 });
}
}
fn main() {
let mut counter = CounterReactor::new(CounterState { count: 0 });
counter.subscribe().listen(|old, new| println!("{} → {}", old.count, new.count));
counter.increment(); // prints: 0 → 1
counter.increment(); // prints: 1 → 2
assert_eq!(counter.state().count, 2);
}Opt in to neutron firing by adding neutrons = YourEvent to the macro. GLoC generates
a fire() method; you write on_event() to handle each variant. Both styles live on the
same reactor — neither replaces the other.
use gloc::{reactor, reactor_state, Reactor};
#[reactor_state]
pub struct CounterState { pub count: i32 }
#[derive(Debug)]
pub enum CounterEvent {
Increment,
Decrement,
AddBy(i32),
Reset,
}
#[reactor(state = CounterState, neutrons = CounterEvent)]
pub struct CounterReactor {}
impl CounterReactor {
fn on_event(&mut self, event: CounterEvent) {
match event {
CounterEvent::Increment => self.emit(CounterState { count: self.state().count + 1 }),
CounterEvent::Decrement => self.emit(CounterState { count: self.state().count - 1 }),
CounterEvent::AddBy(n) => self.emit(CounterState { count: self.state().count + n }),
CounterEvent::Reset => self.emit(CounterState { count: 0 }),
}
}
}
fn main() {
let mut reactor = CounterReactor::new(CounterState { count: 0 });
reactor.subscribe().listen(|old, new| println!("{} → {}", old.count, new.count));
reactor.fire(CounterEvent::Increment); // prints: 0 → 1
reactor.fire(CounterEvent::AddBy(4)); // prints: 1 → 5
reactor.fire(CounterEvent::Reset); // prints: 5 → 0
assert_eq!(reactor.state().count, 0);
}When multiple components or threads need to share one reactor, wrap it in a GlocProvider.
All clones share the same reactor — a mutation from any one is visible to all.
use gloc::{reactor, reactor_state, Reactor, GlocProvider, GlocStream};
use std::sync::{Arc, Mutex};
#[reactor_state]
pub struct CounterState { pub count: i32 }
#[reactor(state = CounterState)]
pub struct CounterReactor {}
impl CounterReactor {
pub fn increment(&mut self) {
self.emit(CounterState { count: self.state().count + 1 });
}
}
fn main() {
let initial = CounterState { count: 0 };
let reactor = Arc::new(Mutex::new(CounterReactor::new(initial.clone())));
let stream = GlocStream::new(initial);
let provider = GlocProvider::new(reactor, stream);
let p1 = provider.clone();
let p2 = provider.clone();
p1.listen(|old, new| println!("{} → {}", old.count, new.count));
p2.update(|r| r.increment()); // p1's listener fires: 0 → 1
p2.update(|r| r.increment()); // p1's listener fires: 1 → 2
assert_eq!(p1.state().count, 2);
assert_eq!(p2.state().count, 2);
}Any Clone + PartialEq + Debug type is automatically a State — no explicit impl needed.
Use #[reactor_state] to skip writing the derives:
use gloc::reactor_state;
// Struct state
#[reactor_state]
pub struct CounterState { pub count: i32 }
// Enum state — great for loading flows
#[reactor_state]
pub enum FetchState { Idle, Loading, Success(String), Error(String) }
// With extra derives
#[reactor_state(derive(Hash, Eq))]
pub struct TagState { pub tag: u32 }use gloc::{reactor, reactor_state, Reactor};
#[reactor_state]
pub struct CounterState { pub count: i32 }
#[reactor(state = CounterState)]
pub struct CounterReactor {}
impl CounterReactor {
pub fn increment(&mut self) {
self.emit(CounterState { count: self.state().count + 1 });
}
pub fn decrement(&mut self) {
self.emit(CounterState { count: self.state().count - 1 });
}
pub fn reset(&mut self) {
self.emit(CounterState { count: 0 });
}
}
let mut r = CounterReactor::new(CounterState { count: 0 });
r.increment();
r.increment();
assert_eq!(r.state().count, 2);Annotate fields with #[state] — the macro generates {ReactorName}State automatically:
use gloc::{reactor, Reactor};
#[reactor]
pub struct ToggleReactor {
#[state] pub active: bool,
}
// Macro generates: pub struct ToggleReactorState { pub active: bool }
impl ToggleReactor {
pub fn toggle(&mut self) {
self.emit(ToggleReactorState { active: !self.state().active });
}
}
let mut toggle = ToggleReactor::new(ToggleReactorState { active: false });
toggle.toggle();
assert!(toggle.state().active);Every #[reactor] struct gets:
| Generated | Description |
|---|---|
impl Reactor |
state(), emit() with change-detection |
new(initial) |
Constructor — suppress with no_new |
fire(neutron) |
Event dispatch — only when neutrons = N is set; calls self.on_event(neutron) |
subscribe() |
Returns a GlocSubscription read-only handle |
attach_listener(l) |
Attaches a GlocListener impl |
| Argument | Effect |
|---|---|
state = SomeType |
Mode A — use an existing type as state |
neutrons = SomeType |
Opt-in event dispatch — generates fire(); you write on_event() |
no_new |
Skip new() generation |
Subscribe to state transitions via subscribe().listen():
let mut r = CounterReactor::new(CounterState { count: 0 });
r.subscribe().listen(|old, new| {
println!("{} → {}", old.count, new.count);
});
r.increment(); // prints: 0 → 1
r.increment(); // prints: 1 → 2
r.emit(CounterState { count: 2 }); // no-op — no printFor typed observers implement GlocListener:
use gloc::GlocListener;
struct Logger;
impl GlocListener<CounterReactor> for Logger {
fn on_transition(&self, old: &CounterState, new: &CounterState) {
println!("{} → {}", old.count, new.count);
}
}
r.attach_listener(Logger);Share a reactor across components or threads using GlocProvider:
use gloc::{reactor, reactor_state, Reactor, GlocProvider, GlocStream};
use std::sync::{Arc, Mutex};
#[reactor_state]
pub struct CounterState { pub count: i32 }
#[reactor(state = CounterState)]
pub struct CounterReactor {}
impl CounterReactor {
pub fn increment(&mut self) {
self.emit(CounterState { count: self.state().count + 1 });
}
}
let reactor = Arc::new(Mutex::new(CounterReactor::new(CounterState { count: 0 })));
let stream = GlocStream::new(CounterState { count: 0 });
let provider = GlocProvider::new(reactor, stream);
let p1 = provider.clone();
let p2 = provider.clone();
p1.listen(|old, new| println!("{} → {}", old.count, new.count));
p2.update(|r| r.increment()); // p1's listener prints: 0 → 1
assert_eq!(p1.state().count, 1);
assert_eq!(p2.state().count, 1);GLoC reactors integrate cleanly with any Rust UI framework. Here is the full counter example using Dioxus 0.7 desktop.
The reactor is stored in a Dioxus Signal — reads register the component as a
subscriber, writes trigger re-renders.
// src/reactors/counter.rs — zero Dioxus imports, pure domain logic
use gloc::Reactor;
use gloc::reactor;
#[derive(Clone, PartialEq, Debug)]
pub struct CounterState {
pub count: i32,
pub label: String,
}
impl CounterState {
pub fn new(count: i32) -> Self {
let label = match count {
i32::MIN..=-1 => "Negative",
0 => "Zero",
1..=9 => "Low",
10..=99 => "Medium",
_ => "High",
}.into();
Self { count, label }
}
}
#[reactor(state = CounterState)]
pub struct CounterReactor {}
impl CounterReactor {
pub fn increment(&mut self) {
self.emit(CounterState::new(self.state().count + 1));
}
pub fn decrement(&mut self) {
self.emit(CounterState::new(self.state().count - 1));
}
pub fn reset(&mut self) {
self.emit(CounterState::new(0));
}
}// src/main.rs — Dioxus wiring
#![allow(non_snake_case)]
mod reactors;
use reactors::{CounterReactor, CounterState};
use dioxus::prelude::*;
use gloc::Reactor;
fn main() { dioxus::launch(App); }
#[component]
fn App() -> Element {
let reactor = use_signal(|| CounterReactor::new(CounterState::new(0)));
rsx! { CounterView { reactor } }
}
#[component]
fn CounterView(reactor: Signal<CounterReactor>) -> Element {
let state = reactor.read().state().clone();
rsx! {
div {
p { "{state.label}: {state.count}" }
button { onclick: move |_| reactor.write().decrement(), "−" }
button { onclick: move |_| reactor.write().reset(), "Reset" }
button { onclick: move |_| reactor.write().increment(), "+" }
}
}
}Run it:
cargo run -p gloc-example-dioxusFull example source: examples/dioxus/
| Crate | Feature | Effect |
|---|---|---|
gloc |
tracing |
Enables tracing::debug! inside emit() — logs every state transition. Zero cost when disabled. |
gloc-macro |
tracing |
Same — gates the tracing call in macro-generated emit(). |
Enable tracing:
[dependencies]
gloc = { version = "0.2", features = ["tracing"] }
tracing = "0.1"
tracing-subscriber = "0.3"Every emit() call that transitions state will log:
DEBUG CounterReactor{old=CounterState { count: 0 }, new=CounterState { count: 1 }}
GLoC/
├── gloc-core/ Core traits — Reactor, State, GlocProvider, GlocStream, GlocListener, GlocObserver
├── gloc-macro/ Proc macros — #[reactor], #[reactor_state]
├── gloc/ Umbrella crate (published as `gloc`)
├── gloc-axum/ Axum HTTP adapter
├── gloc-bevy/ Bevy ECS adapter
├── examples/
│ ├── dioxus/ Desktop UI — Dioxus 0.7
│ ├── axum/ HTTP API — CartReactor + InventoryReactor
│ ├── bevy/ Headless game — PlayerReactor + WaveReactor
│ └── cli/ Terminal REPL — task manager
└── plugins/
├── vscode/ VS Code extension — New GLoC Reactor command
└── intellij/ IntelliJ plugin — New GLoC Reactor action
GLoC welcomes contributions of every kind — from first-time open-source contributors to seasoned Rust experts. No contribution is too small.
The only hard rule: every change must go through a Pull Request and pass the full CI pipeline before it can be merged.
| Type | Examples |
|---|---|
| Bug reports | Something panics unexpectedly, wrong behaviour, misleading error message |
| Documentation | Improve doc comments, fix typos, add usage examples |
| Tests | Add missing test cases, improve coverage, add trybuild fail scenarios |
| Bug fixes | Fix a reported issue, improve edge-case handling |
| New features | New macro arguments, new generated methods |
| Framework adapters | Dioxus, Axum, Bevy, Tauri, Leptos, or any other Rust framework |
git clone https://github.com/<your-username>/gloc.git
cd gloc
git checkout -b feat/your-featureRun the full local check suite before every push:
cargo fmt --all
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo test -p gloc-macro --test ui_tests| Job | Local command |
|---|---|
| build | cargo build --workspace |
| test | cargo test --workspace |
| fmt | cargo fmt --all -- --check |
| clippy | cargo clippy --workspace --all-targets -- -D warnings |
Licensed under the MIT License.
Built with Rust 🦀 — designed for everyone.