GLoC Part 2 — State, Reactors, and the Art of Knowing When Things Change
This is Part 2 of the GLoC series. In Part 1 we built a counter to get a feel for the library. Now we slow down and really understand the…
GLoC Part 2 — State, Reactors, and the Art of Knowing When Things Change

This is Part 2 of the GLoC series. In Part 1 we built a counter to get a feel for the library. Now we slow down and really understand the architecture — because once these concepts click, everything else in GLoC makes sense immediately. Read Part 1 **here**
The Central Question: Who Owns What?
Most state management bugs trace back to a single root cause — state is owned by nobody, or everybody. It gets copied into components, passed through props, cached in globals, mutated from unexpected places. You end up debugging which version of the truth you’re looking at.
GLoC’s answer is deliberate: one slice of state, one owner, one source of truth. That owner is the Reactor.
Let’s build up to it from first principles.
State — Just Data, With Rules
In GLoC, State is any type that satisfies three bounds:
Clone + PartialEq + Debug
That’s the entire contract. No base class. No trait implementation. No registration with a global registry. If your type satisfies those three bounds, it’s a valid GLoC state.
The #[reactor_state] macro exists purely as a convenience — it injects the derives so you don't repeat yourself:
#[reactor_state]
pub struct CartState {
pub items: Vec<String>,
pub total: f64,
}
// Expands to:
#[derive(Clone, PartialEq, Debug)]
pub struct CartState {
pub items: Vec<String>,
pub total: f64,
}
State can be a struct, an enum, or a primitive. GLoC doesn’t care about shape — only the bounds matter:
// Struct — rich domain object
#[reactor_state]
pub struct UserState {
pub name: String,
pub logged_in: bool,
pub role: UserRole,
}
// Enum — perfect for loading flows
#[reactor_state]
pub enum FetchState {
Idle,
Loading,
Success(String),
Error(String),
}
// Even a primitive works
type CounterState = i32;
The enum pattern is particularly powerful. Instead of is_loading: bool + error: Option<String> + data: Option<T> — three fields that can contradict each other — you get one enum where only valid combinations are representable.
Change Detection Is Built In
Here’s where PartialEq earns its place. When you call emit(), GLoC doesn't blindly fire the stream — it compares first:
reactor.emit(CounterState { count: 0 }); // current count is 0 → equal → silent
reactor.emit(CounterState { count: 1 }); // different → stream fires
reactor.emit(CounterState { count: 1 }); // same again → silent
This is subtle but important. It means:
- No spurious re-renders from
emit()calls that don't actually change anything - You can call
emit()defensively — normalize your state and let GLoC figure out if anything changed - Derived computations and side effects only run when they need to
The cost of the comparison is the cost of PartialEq on your state type — typically O(1) for simple structs, predictably bounded for everything else.
Reactor — The Owner
The Reactor is the heart of GLoC. It owns one slice of state, exposes domain methods to mutate it, and carries a built-in reactive stream. Think of it as a self-contained state machine with a broadcast channel built in.
There are two ways to define one.
Mode A — Bring Your Own State
You define the state separately, then attach it to the 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 Mode A when your state type is shared, reused across reactors, or you want to define it independently for testing purposes.
Mode B — Let GLoC Generate the State
You annotate fields directly on the reactor and the macro generates the state struct for you:
#[reactor]
pub struct ToggleReactor {
#[state] pub active: bool,
#[state] pub label: String,
}
// GLoC silently generates:
// #[derive(Clone, PartialEq, Debug)]
// pub struct ToggleReactorState {
// pub active: bool,
// pub label: String,
// }
Use Mode B for quick iteration — fewer files, less ceremony. The generated state type follows the naming convention {ReactorName}State.
What the Macro Actually Generates
Both modes produce the same underlying structure. Understanding what gets generated removes the magic:
**impl Reactor** — Wiresstate(),emit(), andstream()**new(initial)** — Constructor that takes the initial state**impl Deref<Target = State>** — Direct field access:reactor.countinstead ofreactor.state().count**fire(neutron)** — Event dispatcher, only whenneutrons = Nis set
The Deref implementation is the detail most people notice first in practice. Instead of:
let count = reactor.state().count;
You write:
let count = reactor.count; // Deref makes this work
Both are valid. The explicit state() form is clearer when you want to be intentional about "I'm accessing state here." The Deref form is ergonomic for read-heavy code.
Neutron — The Trigger
The name follows GLoC’s nuclear fission metaphor. A neutron strikes the nucleus, causing a chain reaction. In GLoC, a Neutron is an event fired at a reactor.
Any type satisfying Debug + Send + 'static is automatically a Neutron. No registration, no trait implementation:
#[derive(Debug)]
pub enum AuthEvent {
Login { username: String, password: String },
Logout,
RefreshToken,
}
To enable event dispatch on a reactor, add neutrons = YourEvent to the macro:
#[reactor(state = AuthState, neutrons = AuthEvent)]
pub struct AuthReactor {}
impl AuthReactor {
fn on_event(&mut self, event: AuthEvent) {
match event {
AuthEvent::Login { username, .. } => {
self.emit(AuthState {
logged_in: true,
user: username,
});
}
AuthEvent::Logout => {
self.emit(AuthState {
logged_in: false,
user: String::new(),
});
}
AuthEvent::RefreshToken => {
// token refresh logic
}
}
}
}
The macro generates fire(). You write on_event(). The split is intentional — dispatch is infrastructure, the handler is your logic:
reactor.fire(AuthEvent::Login {
username: "alice".into(),
password: "***".into(),
});
reactor.fire(AuthEvent::Logout);
Domain Methods vs Neutrons — When to Use Which?
Both approaches mutate state. Which should you reach for?
Domain methods (reactor.logout()) are better for:
- Simple, direct mutations
- When the caller knows exactly what operation they want
- Internal reactor-to-reactor calls
Neutrons (reactor.fire(AuthEvent::Logout)) are better for:
- Event-driven flows where the reactor decides what to do
- When multiple callers trigger the same logical event
- Cross-thread dispatch (neutrons are
Send) - Decoupling the caller from the reactor’s internal logic
They’re not mutually exclusive — you can expose both on the same reactor:
reactor.logout(); // domain method
reactor.fire(AuthEvent::Logout); // neutron — same result
The Architecture So Far
At this point you have everything you need to model your application’s data layer cleanly:
┌─────────────────────────────────────────┐
│ Reactor │
│ │
│ State ──► emit() ──► change detect │
│ │ │
│ Neutron ──► fire() │ │
│ │ ▼ │
│ on_event() GlocStream │
│ (your logic) │
└─────────────────────────────────────────┘
State is the data. emit() is the only way to change it. fire() is how external events enter the system. GlocStream is how change leaves the reactor and reaches the rest of the world.
We’ll cover GlocStream, sharing, and observation in Part 3.
A Real Example — Shopping Cart
Let’s put it together with something more realistic than a counter. A shopping cart with two modes — enum state for async operations, struct state for the cart itself:
#[reactor_state]
pub enum CartLoadState {
Idle,
Loading,
Ready,
Error(String),
}
#[reactor_state]
pub struct CartState {
pub items: Vec<CartItem>,
pub total: f64,
pub coupon: Option<String>,
}
#[derive(Debug)]
pub enum CartEvent {
AddItem(CartItem),
RemoveItem(String), // item id
ApplyCoupon(String),
Checkout,
}
#[reactor(state = CartState, neutrons = CartEvent)]
pub struct CartReactor {}
impl CartReactor {
fn on_event(&mut self, event: CartEvent) {
match event {
CartEvent::AddItem(item) => {
let mut items = self.items.clone();
items.push(item);
let total = items.iter().map(|i| i.price).sum();
self.emit(CartState { items, total, ..self.state().clone() });
}
CartEvent::RemoveItem(id) => {
let items: Vec<_> = self.items
.iter()
.filter(|i| i.id != id)
.cloned()
.collect();
let total = items.iter().map(|i| i.price).sum();
self.emit(CartState { items, total, ..self.state().clone() });
}
CartEvent::ApplyCoupon(code) => {
self.emit(CartState {
coupon: Some(code),
..self.state().clone()
});
}
CartEvent::Checkout => {
// trigger checkout flow
}
}
}
}
Notice a few things:
- The cart state itself is a struct — it has multiple fields that evolve together
- The load/error lifecycle is a separate enum reactor — they’re different concerns
on_eventis a pure state machine — given an event, produce a new stateemit()only fires the stream if something actually changed — callingAddItemwith a duplicate item that produces the same state is a no-op
Summary
**State** — AnyClone + PartialEq + Debugtype. Use it as your domain data model.**#[reactor_state]** — Derive macro shorthand. Use it on every state type.**Reactor(Mode A)** — State defined separately. Use it for shared/reused state types.**Reactor(Mode B)** — State generated from fields. Use it for fast iteration and simple reactors.**emit()** — The only way to update state. Call it inside reactor methods.**Neutron** — An event fired at a reactor. Use it for event-driven flows and cross-thread dispatch.**fire()** — Dispatches a neutron. Use it from external callers and UI events.
Next up: Part 3 — Streams, Sharing, and Observation. We’ll cover GlocStream, ListenerHandle, GlocProvider, GlocListener, and GlocObserver — the entire reactive layer that makes your data actually move.
GitHub: https://github.com/godwinjk/gloc Crates.io: https://crates.io/crates/gloc
메타데이터
- post_id
- 3a30f99ebaa2
- slug
- gloc-part-2-state-reactors-and-the-art-of-knowing-when-things-change-3a30f99ebaa2
- url
- https://medium.com/@godwinjoseph.k/gloc-part-2-state-reactors-and-the-art-of-knowing-when-things-change-3a30f99ebaa2
- canonical_url
- https://medium.com/@godwinjoseph.k/gloc-part-2-state-reactors-and-the-art-of-knowing-when-things-change-3a30f99ebaa2
- author_url
- https://medium.com/@godwinjoseph.k
- status
- ok
- fetched_at
- 2026-06-17 08:20:12