はじめに
コンポーネント間の通信を作るたびに、Reduxのようなステート管理ライブラリを導入するのは大げさすぎる。かといってCustomEventを自前で組み立てるのも面倒――そんな場面、意外と多くないでしょうか。
Mittは、そのちょうど中間を埋めるライブラリです。on・emit・offの3つのメソッドだけで、Pub/Sub(発行購読)パターンによるイベント駆動の仕組みを組み立てられます。gzip圧縮後わずか200バイトという軽さで、Node.jsのEventEmitterに近い感覚のまま、フレームワークに依存しないイベントバスを手に入れられます。
先にどんな挙動になるのか触ってみたい方は、こちらからどうぞ。
Mittとは
Mittは、developit氏(Preactの作者としても知られています)が公開している、超軽量な関数型イベントエミッタです。npmの週間ダウンロード数は数百万件規模にのぼり、Vue 3の内部やVue Routerのエコシステムなど、多くのライブラリの内部実装にも採用されています。
主な特徴
- 超軽量 - gzip圧縮後200バイト未満というサイズで、バンドルサイズへの影響をほぼ気にせずに導入できます
- ワイルドカード対応 -
"*"をイベントタイプとして登録すると、発火した全イベントを1つのハンドラで受け取れます - 関数型設計 -
thisに依存しないため、クラスベースのAPIより取り回しがよく、TypeScriptとの相性も良好です - ランタイムを選ばない - ブラウザ向けに作られていますが、Node.jsやDenoなど任意のJavaScript環境で動作します
インストール
npm install mitt
yarn add mitt
pnpm add mitt
Mittのサンプルを動かす
以下は、mitt()で作った1つのイベントバスに対して「いいね」と「コメント」という2種類のイベントを流し込むサンプルです。emitter.on()でイベントごとのハンドラを登録し、emitter.emit()で発火させます。合わせて"*"を使ったワイルドカードハンドラも仕込んであり、種類を問わず全イベントをコンソールに出力します。
要点だけを抜き出すと、次のようなコードになります。
import mitt from 'mitt'
const emitter = mitt()
emitter.on('like', (payload) => console.log('like', payload))
emitter.on('*', (type, payload) => console.log('[all]', type, payload))
emitter.emit('like', { postId: 42 })
// 特定のハンドラだけ解除
emitter.off('like')
実際に動かせるものが下です。「いいね」ボタンを押したり、コメント欄にテキストを入力してEnterを押したりすると、その場でログが積み上がっていきます。
emitter.on('like', ...)とemitter.on('*', ...)が同じemit呼び出しに対して両方反応している点に注目してください。コンソールを開くと、ワイルドカードハンドラが型を問わず全イベントを拾っているのが分かります。emitter.off('like')をどこかで呼べば、likeイベントの購読だけを止められます。
基本的な使い方
Mittの使い方は驚くほどシンプルです。mitt()でエミッタインスタンスを作り、on・emit・offの3メソッドだけでイベントの購読・発火・解除を行います。
import mitt from 'mitt'
const emitter = mitt()
// イベントを購読
emitter.on('foo', (e) => console.log('foo', e))
// 全イベントを購読
emitter.on('*', (type, e) => console.log(type, e))
// イベントを発火
emitter.emit('foo', { a: 'b' })
// ハンドラを指定して解除
function onFoo(e) {}
emitter.on('foo', onFoo)
emitter.off('foo', onFoo)
// 特定タイプの全ハンドラを解除
emitter.off('foo')
// 登録済みハンドラを丸ごとクリア
emitter.all.clear()
emitter.allは、イベントタイプとハンドラ配列を対応させたMapです。直接操作することも可能ですが、通常はon/off経由で扱えば十分です。
実践的なユースケース
ワイルドカードで全イベントをロギングする
アプリ内で発生するイベントを横断的に記録したいとき、イベントの種類ごとにログ処理を書くのは非効率です。Mittのワイルドカード("*")ハンドラを使えば、1箇所で全イベントを一括して監視できます。開発中のデバッグや、分析用のイベント収集に向いたパターンです。
3つのボタンはそれぞれ別のイベント名でemitter.emit()していますが、購読側はemitter.on('*', ...)の1行だけで済んでいます。新しいイベント種別を追加しても、ロギング処理側のコードを直す必要がありません。
TypeScriptで型安全なイベントを定義する
イベント名やペイロードの型が揃っていないと、emitする側とonする側で認識がずれてバグになりがちです。Mittはジェネリクスでイベントマップの型を渡せるため、TypeScriptのstrictモードと組み合わせると、存在しないイベント名やペイロードの型不一致をコンパイル時に検出できます。
type Events = {
login: { userId: string }
logout: undefined
}
const emitter = mitt<Events>()
emitter.on('login', (e) => {
console.log(e.userId) // string と推論される
})
emitter.emit('login', { userId: 'u1' })
emitter.emit('logout') // ペイロードなしのイベント
emitter.on('login', ...)のコールバック引数payloadは、Events型に基づいて{ userId: string }と自動推論されます。試しにemitter.emit('login', { userId: 123 })のように数値を渡すコードへ書き換えると、その時点で型エラーになるのが分かるはずです。
イベントリスナーの解除とクリーンアップ
SPAでコンポーネントが何度も生成・破棄される場合、emitter.on()で登録したハンドラを解除し忘れるとメモリリークやイベントの二重発火につながります。Mittはemitter.off()で個別のハンドラを、emitter.all.clear()で全ハンドラを一括解除できます。
const emitter = mitt()
function handleTick() { /* ... */ }
emitter.on('tick', handleTick)
// コンポーネント破棄時などに呼ぶ
emitter.off('tick', handleTick)
// まとめて解除したい場合
emitter.all.clear()
「コンポーネントを破棄」を押すとemitter.off('tick', handler)が呼ばれ、emitter.all.get('tick')で確認できるハンドラ数が0になります。その状態でtickイベントが発火し続けても、カウントが増えなくなることが確認できます。「コンポーネントを追加」を押し忘れたまま何度も破棄ボタンを押しても、handlerがnullかどうかをチェックしているため安全に動作します。
まとめ
Mittは、200バイトという極小サイズながら、on・emit・offとワイルドカード"*"だけでイベント駆動の設計を一通り実現できるライブラリです。大きな状態管理ライブラリを持ち込むほどではないコンポーネント間通信や、横断的なロギング、TypeScriptによる型安全なイベント定義まで、幅広い場面で活躍します。
まずは今のプロジェクトの中で、addEventListenerや自前のコールバック管理で済ませていた箇所をMittに置き換えてみると、その手軽さを実感できるはずです。
