After months of community feedback and real-world usage, we're excited to announce Quo.js v0.5.0, a major release that clarifies our architecture and removes adoption barriers.
Two key changes:
- Event-bus terminology โ API now reflects our event-driven architecture
- MIT License โ Removed enterprise adoption barriers
Let's dive into the technical details and rationale behind these decisions.
What is Quo.js?
Before we discuss what changed, let's clarify what Quo.js is:
Quo.js is an event-driven, async-first state container with atomic subscriptions.
It combines:
- Event-driven architecture with channel-based routing
- FIFO queue for predictable, serialized event processing
- Atomic path subscriptions for zero unnecessary re-renders
- Native async support via built-in middleware and effects
If Redux and Zustand had a baby that grew up studying event sourcing and CQRS, it would be Quo.js.
Part 1: From Dispatch to Emit โ Why Terminology Matters
The Problem with "Dispatch"
When we launched Quo.js, we borrowed Redux's terminology: dispatch(), Action, ActionMap. This made senseโRedux is familiar, and the migration path seemed clear.
But as users adopted Quo.js, confusion emerged:
"Why do I await dispatch? Redux dispatch is synchronous."
"What's the difference between dispatch and emit?"
"Is this just Redux with extra steps?"
The truth: Quo.js isn't a flux dispatcher. It's an event-driven state container with a fundamentally different execution model.
Quo.js's Architecture: Event Bus + FIFO Queue
Let's look at what actually happens when you call store.emit():
// 1. Event is created with unique ID (deduplication)
const event = {
channel: 'todos',
type: 'add',
payload: todo,
id: Symbol()
};
// 2. Event is enqueued in FIFO queue
this.eventQueue.push(event);
// 3. If queue is processing, return (backpressure)
if (this.isProcessingQueue) return;
// 4. Drain queue sequentially
while (this.eventQueue.length) {
const event = this.eventQueue.shift();
// 5. Run async middleware pipeline
for (const mw of this.middleware) {
const ok = await mw(state, event, emit);
if (!ok) break; // Middleware can cancel
}
// 6. Apply reducers (synchronous)
this.reducerBus.emit(event.channel, event.type, event.payload);
// 7. Run async effects
await this.notifyEffects(event);
// 8. Notify subscribers (if state changed)
if (stateChanged) {
this.listeners.forEach(l => l());
}
}
This is not dispatch. This is event emission through a queue with:
- Async pipeline
- Event deduplication
- Serialized processing
- Built-in backpressure
The word "dispatch" implies synchronous, immediate execution. Quo.js does neither.
Enter: Event-Bus Terminology
In v0.5.0, we've adopted terminology that reflects reality:
| Old (v0.4.x) | New (v0.5.0) | Reason |
|---|---|---|
dispatch() |
emit() |
Events are emitted, not dispatched |
Action |
Event |
We emit events, not actions |
ActionMap |
EventMap |
Maps channels to event types |
action.event |
event.type |
Clearer: "the event's type" |
Code example:
// BEFORE (v0.4.x) โ
store.dispatch('analytics', 'track', { page: '/home' });
const reducer = (state, action) => {
if (action.event === 'track') {
// ...
}
};
// AFTER (v0.5.0) โ
store.emit('analytics', 'track', { page: '/home' });
const reducer = (state, event) => {
if (event.type === 'track') {
// ...
}
};
The new terminology instantly communicates:
- Events flow through the system (event sourcing)
- Channels organize events (pub/sub model)
- Emit suggests async, queued processing
Migration Path: Deprecation, Not Deletion
We understand changing terminology is disruptive. That's why we've taken a gradual deprecation approach:
โ
Old APIs still work (with dev warnings)
โ
Type aliases provided (Action = Event)
โ
Clear timeline (removal in v1.0.0)
โ
Comprehensive migration guide
// This still works in v0.5.0 (with warning):
store.dispatch('todos', 'add', todo);
// Console (development only):
// [@quojs/core] `store.dispatch()` is deprecated.
// Use `store.emit()` instead. Will be removed in v1.0.0.
Only one true breaking change: action.event โ event.type
We couldn't alias this one because it would break TypeScript inference. But the fix is straightforward:
// Find and replace in your reducers:
// action.event โ event.type
Why This Matters
Terminology isn't just semanticsโit shapes mental models.
When you see store.emit('analytics', 'track', data), you immediately understand:
- This is asynchronous (like emitting an event)
- It's fire-and-forget (queue handles it)
- It's event-driven (not command-driven like Redux)
This alignment between naming and behavior reduces cognitive load and makes Quo.js easier to understand, especially for developers coming from event-driven systems (Kafka, RabbitMQ, EventEmitter).
Part 2: From MPL-2.0 to MIT โ Removing Adoption Barriers
The License Problem We Didn't See Coming
When we launched Quo.js, we chose Mozilla Public License 2.0 (MPL-2.0) for good reasons:
- File-level copyleft (modifications stay open)
- Patent protection clauses
- Permissive for most use cases
We thought: "MPL-2.0 is permissive! Libraries like Firefox use it!"
Reality check: Enterprise legal departments don't care about nuance.
Over the past months, we heard from multiple teams:
"Our legal team won't approve MPL-2.0. Can you switch to MIT?"
"We can't use Quo.js until the license changes."
"Adding to our project requires a legal reviewโ3-6 weeks minimum."
The pattern was clear: MPL-2.0 was silently killing adoption.
Why Enterprises Fear MPL-2.0
It's not that MPL-2.0 is restrictiveโit's unfamiliar:
- Whitelist policies: Most companies have "MIT/Apache/BSD-only" policies
- Legal bandwidth: Non-standard licenses require review (slow, expensive)
- Risk aversion: Lawyers say "no" to unknowns by default
- Contributor concerns: Open-source contributors prefer MIT for portfolios
Meanwhile, every major JavaScript library uses MIT:
- React, Vue, Angular โ MIT
- Redux, Zustand, Jotai, MobX โ MIT
- Express, Fastify, Next.js โ MIT
We were the outlier.
The Decision: MIT
After consulting with the community, contributors, and users, we made the switch:
Quo.js v0.5.0 is MIT licensed.
What We Gain
โ
Instant enterprise approval โ No legal review needed
โ
Community trust โ Industry-standard terms everyone knows
โ
Contributor friendliness โ MIT is the default for OSS portfolios
โ
Ecosystem alignment โ Matches React, Redux, and every major library
What We Don't Lose
The license is more permissive, not less:
โ
Open-source commitment โ Still 100% open
โ
Copyright ownership โ Contributors still own their work
โ
Community governance โ Unchanged
โ
Commercial use โ Fully allowed (as before)
The only difference: Derivatives don't have to be open-source. But in practice, most Quo.js usage is integration (not forking), so this rarely matters.
A Note on Pragmatism
Some might see this as "selling out" to enterprise demands. We see it differently:
Open-source impact = adoption ร value
If MPL-2.0 blocks 50% of potential users, we're cutting our impact in half. MIT maximizes reach without compromising values.
We're not a protocol (where copyleft matters). We're a library. Our success comes from widespread use, not forced contributions.
MIT is the pragmatic choice for maximum impact.
Part 3: What Didn't Change (And Why That's Good)
Amidst these changes, Quo.js's core value proposition remains exactly the same:
1. Atomic Subscriptions Still Eliminate Re-renders
// Only re-renders when THIS specific todo's title changes
const title = useAtomicProp({
reducer: 'todos',
property: 'items.0.title'
});
// Wildcard patterns for collections
const allTitles = useAtomicProp({
reducer: 'todos',
property: 'items.*.title'
});
This is still the killer feature. No other library offers this by default.
2. Async Pipeline Still Built-in
// Async middleware (no thunks needed)
const authMiddleware = async (state, event, emit) => {
if (event.type === 'login') {
const token = await authenticateUser(event.payload);
await emit('auth', 'loginSuccess', { token });
return false; // Cancel original event
}
return true;
};
// Async effects (no sagas needed)
const analyticsEffect = async (event, getState, emit) => {
await trackEvent(event);
};
Async is first-class, not an afterthought.
3. TypeScript Inference Still Excellent
type AppEM = {
todos: {
add: { id: string; title: string };
toggle: { id: string };
};
};
// Full autocomplete:
await store.emit('todos', 'add', {
id: '1',
title: 'Buy milk'
});
// TypeScript catches errors:
await store.emit('todos', 'add', { id: 1 });
// โ Error: id must be string
Types are inferred, not manually specified.
4. Universal Runtime Still Supported
// Works everywhere:
import { createStore } from '@quojs/core';
// Browser, Node.js, Deno, Bun
const store = createStore({ /* ... */ });
Zero DOM dependencies. Use it in servers, CLI tools, microservices.
Performance: The Numbers Don't Lie
We keep mentioning "zero unnecessary re-renders." Let's prove it.
Benchmark: Todo App (50 items)
Same app. Same interactions. Different libraries.
| Library | Re-renders (Toggling a TODO item) |
|---|---|
| Redux Toolkit | 142 |
| Quo.js | 1 |
Why?
Redux/Zustand re-render every component subscribed to state.todos. Even if the change was to todos[49], components displaying todos[0] re-render.
Quo.js only re-renders components subscribed to the changed path:
// This component ONLY re-renders when items[0].title changes
function TodoItem() {
const title = useAtomicProp({
reducer: 'todos',
property: 'items.0.title'
});
return <div>{title}</div>;
}
// Adding todos[50]? This component doesn't care.
Atomic subscriptions = free performance.
Migration Guide: Step-by-Step
Migrating from v0.4.x to v0.5.0 takes ~15 minutes for most codebases.
Step 1: Update Packages
npm install @quojs/core@0.5.0 @quojs/react@0.5.0
Step 2: Search and Replace
Use your IDE's find-and-replace:
-
dispatch โ emit
-
store.dispatch(โstore.emit( -
useDispatch()โuseEmit()
-
-
action โ event
-
action.eventโevent.type -
: Actionโ: Event(in type annotations)
-
-
ActionMap โ EventMap
-
type AppAMโtype AppEM -
: ActionMapโ: EventMap
-
Step 3: Test
Run your tests. The only breaking change is action.event โ event.type, which TypeScript will catch.
Step 4: Remove Warnings
Check your dev console. You'll see deprecation warnings for any missed renames.
Fix them nowโthey'll be errors in v1.0.0.
Try It Today
Resources:
- ๐ Quo.js Website
- ๐ Core Docs
- ๐ React Docs
- ๐ง Examples
- ๐ Library Comparison
- ๐๏ธ Architecture Deep-Dive
- ๐ฌ GitHub Discussions
Community Feedback Welcome
We built Quo.js because we needed itโbut we're shaping it based on your feedback.
What do you think of v0.5.0?
- Does the event-bus terminology make sense?
- Relieved about MIT?
- What features do you need next?
Drop a comment below or join the discussion on GitHub.
Thank You
To everyone who:
- Provided feedback on terminology
- Shared concerns about MPL-2.0
- Tested RC builds
- Filed issues and PRs
- Used Quo.js in production
Made in ๐ฒ๐ฝ, for the world.
โ The Quo.js Team
P.S. If Quo.js helps you, give us a star on GitHub! โญ
Top comments (0)