DEV Community

quojs
quojs

Posted on

๐Ÿš€ Quo.js v0.5.0 ๐Ÿš€ - Event-Driven Architecture and MIT License

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:

  1. Event-bus terminology โ€” API now reflects our event-driven architecture
  2. 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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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') {
    // ...
  }
};
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Whitelist policies: Most companies have "MIT/Apache/BSD-only" policies
  2. Legal bandwidth: Non-standard licenses require review (slow, expensive)
  3. Risk aversion: Lawyers say "no" to unknowns by default
  4. 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'
});
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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({ /* ... */ });
Enter fullscreen mode Exit fullscreen mode

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

See it for yourself!

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 2: Search and Replace

Use your IDE's find-and-replace:

  1. dispatch โ†’ emit

    • store.dispatch( โ†’ store.emit(
    • useDispatch() โ†’ useEmit()
  2. action โ†’ event

    • action.event โ†’ event.type
    • : Action โ†’ : Event (in type annotations)
  3. 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

Quick Start Guide.

Resources:


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)