Commit No Bug
Published on

Mitt: The Tiny Event Emitter Every React Native Developer Should Know About

Authors
Vibrant landscape tech poster for “Mitt” with a “mitt” icon and neon event-stream lines on a colorful gradient background.

What Mitt Actually Is

mitt is a functional event emitter built by Jason Miller (the creator of Preact). It weighs under 200 bytes gzipped, has zero dependencies, and works in any JavaScript runtime - browser, Node, or Hermes, React Native's own engine. The whole implementation fits comfortably in a single file, which is exactly the kind of tool you want to have for your app's event system: small enough to fully understand and trust.

I used it in my React Native app Voice Timer, where maybe I did overuse it, but it was so helpful in many situations in my case; one of them was to get the data to use in my live history, where I can see multiple cards of timers at a glance, counting down if one timer or multiple timers are on.

With this, I avoided complicated state management. And sure, it is better to have your app architecture where you don't need much of mitt's use. But sometimes it is the tool that feels right in certain situations.

Install it like anything else:

npm install mitt

The Core API

Mitt's entire surface area is three methods on, off, emit, and the all map, a property whose value is a reference to a native Map of event types to handler arrays. That's it. Anything you do beyond that - .clear(), .get('foo') - is Map's own API, not mitt's; mitt just hands you the reference.

import mitt from 'mitt'

const emitter = mitt()

// listen to an event
emitter.on('recognitionResult', (text) => console.log('heard:', text))

// listen to everything
emitter.on('*', (type, event) => console.log(type, event))

// fire an event
emitter.emit('recognitionResult', 'set a timer for five minutes')

// clean up
emitter.all.clear()

The Wildcard Listener Is Underrated

emitter.on('*', handler) catches every event type that passes through the bus, with the event's name as the first argument. This turns out to be a good debugging tool - drop a wildcard listener in during development to log every event flowing through your app, and remove it before shipping:

if (__DEV__) {
  eventBus.on('*', (type, event) => console.log('[eventBus]', type, event))
}

One detail worth knowing: mitt always runs type-matched handlers first, then wildcard handlers - regardless of the order you registered them in. So a debug logger on '*' is guaranteed to fire after your real handlers have already processed the event, not before.

Cleaning Up Listeners

This is the part that trips people up most, and it's not really mitt's fault - it's the same mistake as forgetting to remove any event listener, just easier to miss because mitt makes subscribing so effortless.

The classic bug looks like this:

// don't do this
useEffect(() => {
  eventBus.on('recognitionResult', (payload) => setText(payload))
  // no cleanup - the handler is never removed
}, [])

Two things go wrong here. First, the anonymous function passed to on() has no reference kept anywhere, so there's no way to off() it later even if you wanted to - it just sits in the handler array in all forever, or until all.clear() wipes everything at once. Second, if this component mounts and unmounts repeatedly (a list item scrolling in and out, a screen you navigate to and from), each mount adds another handle without removing the previous one. setText gets called multiple times per event, on state that may belong to an already-unmounted component - which React will warn you about.

The fix is the same pattern used everywhere else in the post: keep a reference to the handler, and remove it in the useEffect cleanup function.

useEffect(() => {
  const handler = (payload) => setText(payload)
  eventBus.on('recognitionResult', handler)
  return () => eventBus.off('recognitionResult', handler)
}, [])

off() matches on the exact function reference, not on what the function does - so eventBus.off('foo', () => {}) with a freshly written arrow function will never remove anything, even if it looks identical to the one you registered. The handler passed to off has to be the same reference passed to on, which is why it needs to be pulled out into a named variable rather than written inline twice.

If you're ever unsure whether a listener actually got cleaned up, eventBus.all.get('recognitionResult')?.length will tell you how many handlers are currently registered for that event - a quick way to catch a leak before it becomes a "why is this firing five times" bug.

Where It Doesn't Fit

Mitt isn't a replacement for state management. It has no concept of current state - only of things that happened. If a component mounts after an event fires, it misses it; there's no replay. For anything where "what is the current value right now" matters, reach for Context, Zustand, or Redux instead. Mitt is for signals, not state.

Conclusion

Mitt is just a small, dependency-free way for cases where one file calls emitter.emit(...), any number of other files call emitter.on(...), and neither side has to import or know about the other.

Sometimes it's genuinely hard to tell one part of an app that something happened in a completely different, unrelated part - the two pieces might not share a parent component, might not both be wrapped in the same Context, or might just be far enough apart in the tree that passing data between them the normal way gets awkward. That's exactly where mitt shines.

Found this helpful? Please share it with someone who also might find it helpful. Would you like more posts from me? Subscribe to the newsletter. Got questions or comments? Send an email to commitnobug@outlook.com.