mitt and nanoevents are tiny untyped (or loosely typed) event emitters. They scale badly the moment the same event has more than one handler, or handlers need to read state, or you want to debounce. Triggery is what you’d build if you took those problems seriously.
mitt / nanoevents Triggery emitter.on('foo', fn)useAction(trigger, 'foo', fn) (or useEvent on the firing side)emitter.emit('foo', payload)fireFoo(payload) from useEvent(trigger, 'foo')emitter.off('foo', fn)Automatic — cleanup on component unmount Untyped payload Schema['events']['foo'] enforced at compile timeMultiple handlers per event Multiple reactors registering the same action; or multiple triggers listening to the same event Manual debounce/throttle around emit actions.debounce(ms).foo?.(p)One global emitter Scoped via <TriggerScope id="…"> for multi-tenant trees
Before — mitt
import mitt from 'mitt' ;
type Events = { 'cart:added' : { sku : string }; 'cart:removed' : string };
export const bus = mitt < Events >();
Before — usage
bus. on ( 'cart:added' , e => analytics. track ( 'add' , e. sku ));
bus. on ( 'cart:added' , e => sidebar. flash (e. sku ));
// somewhere else:
bus. emit ( 'cart:added' , { sku : 'A1' });
After
export const cartTrigger = createTrigger <{
events : { 'cart:added' : { sku : string }; 'cart:removed' : string };
actions : { trackAddition : { sku : string }; flashSidebar : { sku : string } };
}>({
id : 'cart-added' ,
events : [ 'cart:added' ],
required : [],
handler ({ event , actions }) {
actions. trackAddition ?.({ sku : event. payload . sku });
actions. flashSidebar ?.({ sku : event. payload . sku });
},
});
Reactors (in their own features)
// analytics:
useAction (cartTrigger, 'trackAddition' , ({ sku }) => analytics . track ( 'add' , sku));
// sidebar:
useAction (cartTrigger, 'flashSidebar' , ({ sku }) => sidebar . flash (sku));
// producer:
const fireAdded = useEvent (cartTrigger, 'cart:added' );
fireAdded ({ sku : 'A1' });
The trigger file lists the whole fan-out in one place — no hunting bus.on('cart:added' across the codebase.
Before
bus. on ( 'cart:added' , e => {
if (user. role === 'guest' ) guestAnalytics. track (e. sku );
else userAnalytics. track (user. id , e. sku );
});
After
createTrigger <{
events : { 'cart:added' : { sku : string } };
conditions : { user : { id : string ; role : 'guest' | 'user' } };
actions : { trackGuest : string ; trackUser : { userId : string ; sku : string } };
}>({
id : 'cart-analytics' ,
events : [ 'cart:added' ],
required : [ 'user' ],
handler ({ event , conditions , actions }) {
if ( ! conditions. user ) return ;
if (conditions. user . role === 'guest' ) {
actions. trackGuest ?.(event. payload . sku );
} else {
actions. trackUser ?.({ userId : conditions. user . id , sku : event. payload . sku });
}
},
});
User identity is registered once as a condition by whichever feature owns auth; the cart trigger reads it lazily.
Before
const debouncedFlush = throttle (() => bus. emit ( 'cart:flush' ), 300 );
input. addEventListener ( 'input' , debouncedFlush);
After
// trigger:
createTrigger <{ events : { 'cart:flush' : void }; actions : { saveCart : void } }>({
id : 'cart-flush' ,
events : [ 'cart:flush' ],
required : [],
handler ({ actions }) {
actions. debounce ( 300 ). saveCart ?.();
},
});
// producer (no manual throttle):
const fireFlush = useEvent (cartTrigger, 'cart:flush' );
input. addEventListener ( 'input' , () => fireFlush ());
The runtime owns the timer. Tests can drive time deterministically with the fakeScheduler .
mitt has no concept of scope — for multi-tenant or micro-frontend setups you usually create one bus per tenant and pass it through context. Triggery handles this with <TriggerScope id="…">:
< TriggerScope id = "tenant-a" >
< CartFeature /> { /* useCondition/useAction registrations live in this scope */ }
</ TriggerScope >
< TriggerScope id = "tenant-b" >
< CartFeature /> { /* parallel, isolated */ }
</ TriggerScope >
The trigger declaration adds scope: 'tenant-a' (or 'tenant-b') and the runtime keeps the registries separate. See Scopes .
Events Typed event bus entries. Scopes Per-tenant / per-feature isolation.