The G is intentional. GLoC started as a hobby project called Godwin's 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,
exposes domain methods that transition it, and carries a built-in reactive stream that
broadcasts every real transition to all subscribers automatically.
┌─────────────────────────────────────────────────────────────┐
│ 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
- Ecosystem
- Quick Start
- Define State
- Define a Reactor
- Reactive Stream
- Observers
- Dioxus Integration
- Feature Flags
- Project Structure
- Contributing
- License
A Reactor owns one slice of domain state, exposes methods to mutate it, and
carries a built-in GlocStream — a fan-out reactive stream that broadcasts
every real transition to all subscribers automatically.
#[reactor(state = CounterState)]
pub struct CounterReactor {}
impl CounterReactor {
pub fn increment(&mut self) {
self.emit(CounterState { count: self.count + 1 });
// emit() → stream fires → all subscribers notified
}
}Unlike Flutter Bloc which has separate Cubit and Bloc types, GLoC has one:
a Reactor supports both direct method calls and event dispatch via fire().
A Neutron is an immutable event fired at a reactor. The name follows GLoC's nuclear fission theme: a neutron strikes the reactor and causes a reaction.
Any type satisfying Debug + Send + 'static is automatically a Neutron:
#[derive(Debug)]
pub enum CounterEvent { Increment, Decrement, AddBy(i32), Reset }
impl CounterReactor {
fn on_event(&mut self, event: CounterEvent) {
match event {
CounterEvent::Increment => self.emit(CounterState { count: self.count + 1 }),
CounterEvent::Decrement => self.emit(CounterState { count: self.count - 1 }),
CounterEvent::AddBy(n) => self.emit(CounterState { count: self.count + n }),
CounterEvent::Reset => self.emit(CounterState { count: 0 }),
}
}
}
reactor.fire(CounterEvent::Increment);Any Clone + PartialEq + Debug type is automatically a State. Use #[reactor_state]
to skip writing the derives:
#[reactor_state]
pub struct CounterState { pub count: i32 }GLoC performs change detection: emitting a value equal to the current state is a no-op — no stream notification, no re-render.
| Concept | Description |
|---|---|
GlocStream |
Built-in fan-out stream on every reactor. Notifies all subscribers on every real transition. |
ListenerHandle |
RAII cancel token returned by every listen() call. Drop to cancel automatically. |
GlocProvider |
Arc<Mutex<R>> wrapper for shared multi-owner reactor access across threads. |
GlocListener |
Typed trait for old → new observation on a specific reactor. |
GlocObserver |
Global hook — sees every reactor in the app. Supports both Debug strings and typed &dyn Any. |
Note: GLoC has not yet been published to crates.io. Add it as a git dependency for now:
[dependencies]
gloc = { git = "https://github.com/godwinjk/gloc" }For Dioxus desktop:
[dependencies]
gloc = { git = "https://github.com/godwinjk/gloc" }
gloc-dioxus = { git = "https://github.com/godwinjk/gloc" }
dioxus = { version = "0.7", features = ["desktop"] }With tracing:
[dependencies]
gloc = { git = "https://github.com/godwinjk/gloc", features = ["tracing"] }
tracing = "0.1"Official IDE plugins for GLoC — generate reactors, states, and events without boilerplate.
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 reactor = CounterReactor::new(CounterState { count: 0 });
// Subscribe to the reactor's built-in stream
// Returns a ListenerHandle — keep it alive to keep listening
let _h = reactor.stream().listen(|old, new| {
println!("{} → {}", old.count, new.count);
});
reactor.increment(); // prints: 0 → 1
reactor.increment(); // prints: 1 → 2
assert_eq!(reactor.state().count, 2);
} // _h dropped → listener cancelleduse 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.count + 1 }),
CounterEvent::Decrement => self.emit(CounterState { count: self.count - 1 }),
CounterEvent::AddBy(n) => self.emit(CounterState { count: self.count + n }),
CounterEvent::Reset => self.emit(CounterState { count: 0 }),
}
}
}
fn main() {
let reactor = CounterReactor::new(CounterState { count: 0 });
let _h = reactor.stream().listen(|_, new| println!("count: {}", new.count));
reactor.fire(CounterEvent::Increment); // count: 1
reactor.fire(CounterEvent::AddBy(4)); // count: 5
reactor.fire(CounterEvent::Reset); // count: 0
}When multiple threads or components need to share one reactor, wrap it in GlocProvider:
use std::sync::{Arc, Mutex};
use gloc::{reactor, reactor_state, Reactor, GlocProvider};
// ... reactor definition ...
fn main() {
let provider = GlocProvider::new(Arc::new(Mutex::new(
CounterReactor::new(CounterState { count: 0 })
)));
let p1 = provider.clone(); // cheap Arc clone — same reactor
let p2 = provider.clone();
// Listen through the shared stream — keep handle alive
let _h = 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);
}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.count + 1 }); }
pub fn decrement(&mut self) { self.emit(CounterState { count: self.count - 1 }); }
pub fn reset(&mut self) { self.emit(CounterState { count: 0 }); }
}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.active });
}
}| Generated | Description |
|---|---|
impl Reactor |
state(), emit() with change-detection, stream() |
new(initial) |
Constructor + fires GlocObserver::on_create |
fire(neutron) |
Event dispatch — only when neutrons = N is set |
impl Deref<Target = State> |
Access state fields directly: reactor.count |
| 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 |
Every reactor carries a built-in GlocStream. Subscribe to it for fan-out
reactive notifications:
let reactor = CounterReactor::new(CounterState { count: 0 });
// Multiple subscribers — all fire on every emit()
let _h1 = reactor.stream().listen(|_, new| println!("UI: {}", new.count));
let _h2 = reactor.stream().listen(|old, new| log::info!("{old:?} → {new:?}"));
reactor.increment(); // both listeners fireListenerHandle — listen() returns a handle. Drop it to cancel:
{
let _h = reactor.stream().listen(|_, new| println!("{}", new.count));
reactor.increment(); // fires
} // _h dropped → listener cancelled
reactor.increment(); // silentCall handle.forget() to keep the listener permanently:
reactor.stream().listen(|_, new| println!("{}", new.count)).forget();Close signal — get notified when a reactor shuts down:
let _h = reactor.stream().on_close(|| println!("reactor closed — cleaning up"));
let provider = GlocProvider::new(Arc::new(Mutex::new(reactor)));
provider.release(); // → on_close() fires, stream.close() fires callbacksReactor-to-reactor — one reactor subscribes to another:
// OrderReactor watches CartReactor
let _h = cart.stream().listen(move |_, new| {
if new.status == CartStatus::CheckedOut {
order.emit(OrderState::placed());
}
});
// Clean up when cart is gone
let _close = cart.stream().on_close(|| println!("cart gone"));use gloc::GlocListener;
struct Logger;
impl GlocListener<CounterReactor> for Logger {
fn on_transition(&self, old: &CounterState, new: &CounterState) {
println!("{} → {}", old.count, new.count);
}
}
let provider = GlocProvider::new(Arc::new(Mutex::new(counter)));
let _h = provider.attach_listener(Logger);
provider.update(|r| r.increment()); // prints: 0 → 1Observe every reactor in the app from one place. Two methods for transitions:
use gloc::{GlocObserver, set_observer};
struct AppLogger;
impl GlocObserver for AppLogger {
// Debug-formatted strings — simple logging
fn on_transition(&self, reactor: &str, old: &str, new: &str) {
println!("[{reactor}] {old} → {new}");
}
// Typed state — structured analytics, downcast to real types
fn on_change(&self, reactor: &str, _old: &dyn std::any::Any, new: &dyn std::any::Any) {
if let Some(s) = new.downcast_ref::<CounterState>() {
println!("counter is now {}", s.count);
}
}
fn on_create(&self, reactor: &str) { println!("[{reactor}] created"); }
fn on_close(&self, reactor: &str) { println!("[{reactor}] closed"); }
}
fn main() {
set_observer(AppLogger); // once, before any reactor is created
// ...
}gloc-dioxus connects reactors to Dioxus with zero prop drilling.
The stream→signal bridge is automatic — any emit() call updates the Dioxus
signal and schedules a re-render without any manual wiring.
use dioxus::prelude::*;
use gloc::{reactor, reactor_state, Reactor};
use gloc_dioxus::{gloc_builder, use_gloc, use_gloc_provide};
#[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.count + 1 }); }
pub fn decrement(&mut self) { self.emit(CounterState { count: self.count - 1 }); }
}
#[component]
fn App() -> Element {
// Provide once at the root — accessible anywhere in the tree
use_gloc_provide(|| CounterReactor::new(CounterState { count: 0 }));
rsx! { Counter {} }
}
#[component]
fn Counter() -> Element {
let counter = use_gloc::<CounterReactor>(); // no prop drilling
// gloc_builder! re-runs closure on every emit() — no manual signal.set()
gloc_builder!(CounterReactor, |state| rsx! {
div {
p { "Count: {state.count}" }
button { onclick: move |_| counter.update(|r| r.decrement()), "−" }
button { onclick: move |_| counter.update(|r| r.increment()), "+" }
}
})
}
fn main() { dioxus::launch(App); }Full showcase with 5 pages:
cargo run -p gloc-example-dioxus| Page | Feature |
|---|---|
| /counter | gloc_builder! — rebuilds on every emit |
| /neutrons | gloc_builder!(when:) — rebuild guard + neutron dispatch |
| /theme | gloc_consumer!(build_when:, listen_when:) — both guards |
| /cart | gloc_listener!(when:) — side effect gated on status transition |
| sidebar | Mode B #[reactor] — shared across all pages |
| Crate | Feature | Effect |
|---|---|---|
gloc |
tracing |
tracing::debug! inside emit() — logs every state transition. Zero cost when disabled. |
GLoC/
├── gloc-core/ Reactor, State, GlocStream, GlocProvider, GlocListener, GlocObserver
├── gloc-macro/ #[reactor], #[reactor_state]
├── gloc/ Umbrella crate
├── gloc-test/ ReactorTester + reactor_test! macro
├── gloc-dioxus/ Dioxus adapter — use_gloc_provide, use_gloc, gloc_builder!
├── gloc-axum/ Axum adapter — AxumReactor, new_axum_state
├── gloc-bevy/ Bevy adapter — GlocPlugin, GlocResource
└── examples/
├── dioxus/ Desktop UI — 5-page feature showcase
├── axum/ HTTP API — CartReactor + InventoryReactor
├── bevy/ Headless game — PlayerReactor + WaveReactor
└── cli/ Terminal REPL — task manager
GLoC welcomes contributions of every kind.
The only hard rule: every change must go through a Pull Request and pass CI.
# Clone and branch
git clone https://github.com/<your-username>/gloc.git
cd gloc
git checkout -b feat/your-feature
# Full local check suite
cargo fmt --all
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo test -p gloc-macro --test ui_tests| CI 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.
github.com/godwinjk/gloc · crates.io/crates/gloc · docs.rs/gloc