EventEmitter、フラッシュメッセージ1を書く時とかに便利ですよね
長々と親コンポーネントから、フラッシュメッセージを呼び出すためのメッセージをバケツリレーで渡す必要もないので、コードが綺麗にまとまります。
これにTypeScriptを使って型定義をしてあげるともっと快適に、型安全に使えたのでまとめてみました
舞台設定
例として、今回は2つのアクションをEventEmitterに登録する場合を考えてみました。
- 名前を引数に渡して、「
Hello 名前
」を出力するだけのアクション - メッセージと(オプションで)関数を渡して、フラッシュメッセージを出すアクション
ポイントは2つの関数で、「渡す引数が個数も型も違う」ということです。
もし型を定義しないと
- EventEmitterって、全部でどんなアクションがあったっけ?
- このアクションに渡す引数ってこれであってたっけ?
みたいなことになりかねませんが、型を定義すればそんな杞憂ともおさらばです!
ではさっそく書いてみましょう!
用意するコード
2種類あります
- EventEmitterに登録する全てのアクションの型定義
- アクションがemitされた時に、トリガーされるロジック
なお、前提としてこれら二つはアプリ全体から参照できる場所で定義するものとします(create-react-app
ならsrc/App.js
)
EventEmitterのアクション型定義
まず確認しておきますと、EventEmitter.emit
というメソッド自体は「一つ」です。
そのメソッドに対し、TypeScriptの型を複数個定義してあげることで、あたかも「複数のメソッドを定義」しているかのように振舞わせることができます
(大元のEventEmitter.emitのドキュメントはこちら)
// myEventEmitter.ts
import { EventEmitter as OriginalEventEmitter } from 'events'
// アクション一覧。必ずオーバーロードしている型と一致させること!!
type EventType = 'flash' | 'greeting'
export interface FlashAction {
label: string
onClick: () => void
}
class EventEmitter extends OriginalEventEmitter {
// 使うアクションの型をオーバーロード
public emit(
event: 'flash',
msg: string,
action?: FlashAction,
): boolean
public emit(event: 'greeting', name: string): boolean
// メソッド本体。元々のemitメソッドを踏襲している
public emit(event: EventType, ...args: any[]) {
return super.emit(event, ...args)
}
}
export const eventEmitter = new EventEmitter()
EventEmitterアクションの発生時のアクション
私はロジックを、引数にeventEmitter
を渡す形のカスタムフック2にまとめています
ここでeventEmitter.addListener
を書きます
残念ながら現時点では、
addListner
内にまでさっき定義した型を適応する方法がありません...。なので、アクションの修正をする際には、「eventEmitter
定義元」と「addListener
」、両方を手動で直す必要があります
import { EventEmitter } from 'events'
import { useEffect } from 'react'
export const useGreeting = (eventEmitter: EventEmitter) => {
useEffect(() => {
eventEmitter.addListener('greeting', (name: string) => {
console.log('hello', name)
})
}, [eventEmitter])
}
import { EventEmitter } from 'events'
import { useEffect, useState } from 'react'
import { FlashAction } from 'src/config/eventEmitter'
// フラッシュ表示は3秒間
const FLASH_INTERVAL_SEC = 3
export const useFlash = (eventEmitter: EventEmitter) => {
const [flashAction, setFlashAction] = useState<FlashAction>()
const [flashMsg, setFlashMsg] = useState('')
const [isFlashOpen, setIsFlashOpen] = useState(false)
// 自動でフラッシュメッセージが消えるロジック
useEffect(() => {
if (!isFlashOpen) return
setTimeout(() => {
setIsFlashOpen(false)
}, FLASH_INTERVAL_SEC * 1000)
}, [isFlashOpen])
useEffect(() => {
eventEmitter.addListener(
'flash',
(msg: string, action?: FlashAction) => {
setFlashAction(action)
setFlashMsg(msg)
setIsFlashOpen(true)
},
)
}, [eventEmitter])
return { flashAction, flashMsg, isFlashOpen, setIsFlashOpen }
}
これらのロジックを上位階層に配置する
あとはこんな感じでロジックを呼び出します。
flashメッセージの表示には当然コンポーネントも必要なので、こちらも同じく上位階層に定義します
(今回は省略しますが、ニーズがあれば書きます)
import { eventEmitter } from './myEventEmitter.ts'
import { useFlash } from './useFlash'
import { useGreeting } from './useGreeting'
useGreeting(eventEmitter)
const {
flashAction,
flashMsg,
isFlashOpen,
setIsFlashOpen
} = useFlash(eventEmitter)
// こんな感じのコンポーネントがあるとして
return (
<FlashMessage
flashAction={flashAction}
flashMsg={flashMsg}
isFlashOpen={isFlashOpen}
setIsFlashOpen={setIsFlashOpen}
/>
)
あとは任意のコンポーネントから以下のように呼び出せばアクションが起こせます!
当然型チェックつき!
import { eventEmitter } from './myEventEmitter.ts'
eventEmitter.emit('greeting', 'john')
// actionはあってもなくてもいい
eventEmitter.emit('flash', 'saved!')
eventEmitter.emit('flash', 'download ready!', () => { downloadCSV() })
終わりに
いかがでしたでしょうか?
型を定義するだけで、一気に使いやすくなるのですごいおすすめです!
皆さんもぜひTypeScriptで良いEventEmitを!