背景
Reactでは、通常、親から子へ props を使ってデータやイベントを伝達します。しかし、親子関係にないコンポーネント間でイベントをやり取りしたい場合もあります。
Vue.jsでのEventBusばかり記事があったので、Reactでの簡単な実装例を備忘録として残しておこうと思います。
この記事では、ReactでEventBus(イベントエミッタ)を作成し、親子関係にないコンポーネント間でイベントを管理する方法を説明します。
さらに、モーダルを開閉する具体例を通して、この仕組みの使い方を解説します。
EventEmitter(イベントエミッタ)とは?
EventEmitter(イベントエミッタ)は、異なるコンポーネント間でイベントを通知するための仕組みです。
EventBus は、アプリケーション全体で利用できるイベント管理の中心的な存在として機能します。イベントを「発火(emit)」し、他のコンポーネントがそのイベントを「リスン(on)」することで、コンポーネント間の連携が実現します。
実装の概要
今回は、モーダルの開閉を EventBus を使って管理します。
この方法を使えば、モーダルの表示や非表示をどこからでもトリガーでき、特定のコンポーネントに依存せず操作できます。
1. イベントエミッタの実装
まずは、EventBus を定義します。
EventEmitter3 を使用し、イベントの型定義も行います。
import EventEmitter from 'eventemitter3';
import StrictEventEmitter from 'strict-event-emitter-types';
// モーダルを表示する際の引数型
export interface OpenModalArgs {
element: React.ReactNode; // 表示するコンポーネント
}
// イベントの型定義
interface Events {
openModal: (args: OpenModalArgs) => void;
closeModal: () => void;
}
// EventBusの作成
export const EventBus: StrictEventEmitter<EventEmitter, Events> = new EventEmitter();
ここで EventBus を定義し、openModal と closeModal という2つのイベントを持つイベントバスを作成しました。これにより、モーダルを開く・閉じるイベントをどこからでも発火できます。
2. モーダルイベント用フックの作成
次に、EventBus を使ってモーダルを開閉するためのカスタムフック useModalEvent を作成します。
このフックは、モーダルを管理するためのコンテキスト(useModalContext)と連携し、モーダルの開閉イベントに応じて処理を行います。
import { useEffect } from 'react';
import { EventBus } from './event';
// モーダル用のカスタムフック、openModal と closeModal を引数として受け取る
export const useModalEvent = (
openModal: (args: { element: React.ReactNode }) => void,
closeModal: () => void
) => {
useEffect(() => {
// モーダルを開くイベントのリスナーを設定
const _openModal = ({ element }: { element: React.ReactNode }) => {
openModal({ element });
};
// モーダルを閉じるイベントのリスナーを設定
const _closeModal = () => {
closeModal();
};
// EventBusにイベントリスナーを登録
EventBus.on('openModal', _openModal);
EventBus.on('closeModal', _closeModal);
// コンポーネントのアンマウント時にリスナーを解除
return () => {
EventBus.removeListener('openModal', _openModal);
EventBus.removeListener('closeModal', _closeModal);
};
}, [openModal, closeModal]);
};
- このフックは、openModal と closeModal の関数を受け取り、EventBus から発火されたイベントに応じてそれらの関数を呼び出します。
- useEffect でイベントリスナーを設定し、コンポーネントのアンマウント時にリスナーを解除することでメモリリークを防ぎます。
3. モーダル表示のコンポーネント
次に、useModalEvent を使ってモーダルを管理するコンポーネントを作成します。このコンポーネントは、モーダルの表示状態と内容を管理します。
import React, { useState } from 'react';
import { useModalEvent } from './useModalEvent';
const ModalContainer = () => {
const [isOpen, setIsOpen] = useState(false);
const [modalContent, setModalContent] = useState<React.ReactNode | null>(null);
const openModal = ({ element }: { element: React.ReactNode }) => {
setModalContent(element);
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
setModalContent(null);
};
// useModalEvent に openModal と closeModal を渡す
useModalEvent(openModal, closeModal);
return (
<div>
{isOpen && (
<div className="modal">
<div className="modal-content">
{modalContent}
<button onClick={closeModal}>Close</button>
</div>
</div>
)}
</div>
);
};
export default ModalContainer;
- ModalContainer コンポーネントは、
openModal
とcloseModal
をuseModalEvent
フックに渡し、EventBus からのイベントに応じてモーダルの表示・非表示を切り替えます。 -
modalContent
の状態は、モーダルに表示する内容を保持します。
4. モーダルをトリガーするコンポーネント
最後に、モーダルを開くためのボタンを実装します。
このコンポーネントは、EventBus.emit
を使って openModal
イベントを発火します。
import { EventBus } from './event';
const TriggerButton = () => {
const handleClick = () => {
EventBus.emit('openModal', { element: <p>モーダルの内容</p> });
};
return <button onClick={handleClick}>モーダルを開く</button>;
};
export default TriggerButton;
まとめ:EventBusを作って、親子関係にないコンポーネント間でModalを制御する
このパターンでは、EventBus を使って親子関係にないコンポーネント間でモーダルの開閉を管理する方法を紹介しました。これにより、グローバルな状態管理や特定のコンポーネント間の密結合を避け、どのコンポーネントからでも簡単にモーダルを制御できます。
EventBus のようなイベント駆動の仕組みは、モーダル管理だけでなく、他の様々なイベント管理にも応用できます。今後のプロジェクトで、今回紹介した方法を活用してみてください。
メリット
- 親子関係にないコンポーネント間でイベントをやり取りできる。
- モーダル管理ロジックが独立しており、再利用性が高い。
- コンポーネント間の密結合を避け、柔軟にモーダルを制御できる。
お疲れ様でした。
わからないところ、間違っているところ、もっといい方法がある場合は、コメントでもDMでも教えてください。