react-modal、react-modal-hook、およびreact-modal-promiseはすべてReactでモーダル(ダイアログ)を実装するためのライブラリですが、設計思想と使用方法が大きく異なります。react-modalは最も古くからある汎用的なモーダルコンポーネントで、状態管理は開発者側で行います。react-modal-hookはReact Hooksのパラダイムに則り、モーダルの表示・非表示ロジックをカスタムフックとして抽象化します。一方、react-modal-promiseはモーダルの結果をPromiseで扱えるように設計されており、ユーザーの選択(例:「OK」または「キャンセル」)を非同期処理として扱うユースケースに特化しています。
モーダルダイアログはWebアプリケーションでよく使うUIパターンですが、Reactでは状態管理やアクセシビリティ対応が意外と面倒です。この記事では、代表的な3つのモーダルライブラリ — react-modal、react-modal-hook、react-modal-promise — の技術的違いを、実際のコードを交えて詳しく解説します。
react-modal は「コンポーネントベース」のアプローチを取ります。モーダル自体がReactコンポーネントであり、親コンポーネントがisOpenプロパティで表示・非表示を制御します。これは最も直感的で、多くの開発者が最初に思いつく形です。
react-modal-hook は「フック中心」の設計です。モーダルの表示状態と制御ロジックをカスタムフックに閉じ込め、コンポーネント側は純粋にUIのレンダリングに集中できます。
react-modal-promise は「Promiseベース」のインターフェースを提供します。モーダルを開く関数がPromiseを返し、ユーザーの操作結果をawaitで受け取れます。これはアラートや確認ダイアログのような単純なインタラクションに最適です。
react-modal: コンポーネントとして直接使うimport React, { useState } from 'react';
import Modal from 'react-modal';
// アプリ全体で一度だけ設定
Modal.setAppElement('#root');
function App() {
const [modalIsOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>モーダルを開く</button>
<Modal
isOpen={modalIsOpen}
onRequestClose={() => setIsOpen(false)}
contentLabel="Example Modal"
>
<h2>こんにちは!</h2>
<button onClick={() => setIsOpen(false)}>閉じる</button>
</Modal>
</div>
);
}
react-modal-hook: フックで状態を管理import React from 'react';
import { useModal } from 'react-modal-hook';
function MyModal({ hide }) {
return (
<div className="modal">
<h2>こんにちは!</h2>
<button onClick={hide}>閉じる</button>
</div>
);
}
function App() {
const [showModal, hideModal] = useModal(() => (
<MyModal hide={hideModal} />
));
return (
<button onClick={showModal}>モーダルを開く</button>
);
}
react-modal-promise: Promiseで結果を受け取るimport React from 'react';
import { modal } from 'react-modal-promise';
function ConfirmModal({ onResolve, onReject }) {
return (
<div className="modal">
<p>本当に削除しますか?</p>
<button onClick={() => onResolve(true)}>はい</button>
<button onClick={() => onReject()}>いいえ</button>
</div>
);
}
async function handleDelete() {
try {
const result = await modal(ConfirmModal);
if (result) {
// 削除処理
console.log('削除しました');
}
} catch (e) {
// キャンセルされた場合
console.log('キャンセルされました');
}
}
function App() {
return <button onClick={handleDelete}>削除</button>;
}
複数のモーダルを同じ画面で使う場合、それぞれの状態をどう管理するかが重要です。
react-modal では、各モーダルごとにuseStateフックを用意する必要があります。
const [isInfoOpen, setInfoOpen] = useState(false);
const [isSettingsOpen, setSettingsOpen] = useState(false);
react-modal-hook は、フックを複数回呼び出すだけで自然に独立した状態を管理できます。
const [showInfo, hideInfo] = useModal(() => <InfoModal hide={hideInfo} />);
const [showSettings, hideSettings] = useModal(() => <SettingsModal hide={hideSettings} />);
react-modal-promise は、各モーダルを別々の関数として定義し、それぞれを独立して呼び出せます。
const showInfo = () => modal(InfoModal);
const showSettings = () => modal(SettingsModal);
モーダルはキーボード操作やスクリーンリーダー対応が必須ですが、各ライブラリの対応状況は異なります。
react-modal は、aria属性やフォーカストラップを自動で処理してくれます(ただしsetAppElementの設定が必要)。これは大きな利点です。
react-modal-hook と react-modal-promise は、モーダルのDOM構造や振る舞いを完全に開発者に委ねるため、アクセシビリティ対応は自前で実装する必要があります。例えば、モーダル表示時にフォーカスを内部に移動させ、Escキーで閉じられるようにするといった処理です。
react-modal は、スタイルやアニメーションを細かく制御できます。overlayClassNameやclassNameでCSSクラスを指定でき、トランジションも容易です。
react-modal-hook は、モーダルのルート要素を自由に設計できるため、ポータルを使わず通常のDOM階層内に配置することも可能です(ただし、通常はReactDOM.createPortalを使います)。
react-modal-promise は、モーダルの見た目よりも「結果をどう扱うか」に焦点を当てているため、UIのカスタマイズは他の2つほど自由ではありません。ただし、渡すコンポーネント次第では十分柔軟です。
ReduxやZustandなどの状態管理ライブラリと連携する場合:
react-modal は、グローバル状態からisOpenを読み取って制御できます。react-modal-hook は、フック内でグローバル状態を参照・更新できますが、少し工夫が必要です。react-modal-promise は、モーダルの結果をグローバル状態に反映する処理をonResolve内で書くのが自然です。執筆時点(2024年)で、いずれのパッケージも公式に非推奨とはされておらず、GitHubリポジトリもアクティブに更新されています。ただし、react-modalは歴史が長く、TypeScript対応や最新のReact機能への追従がやや遅れる傾向があります。一方、react-modal-hookとreact-modal-promiseは比較的新しく、Hooks時代のベストプラクティスに沿った設計になっています。
ユーザー登録や設定変更など、入力項目が多く状態が複雑なモーダルの場合:
react-modalヘルプ、通知、フィルタ設定など、小さなモーダルをいくつも使う場合:
react-modal-hook「保存しますか?」や「削除してもよろしいですか?」のような単純な選択を行う場合:
react-modal-promise| 特徴 | react-modal | react-modal-hook | react-modal-promise |
|---|---|---|---|
| 設計思想 | コンポーネントベース | フックベース | Promiseベース |
| 状態管理 | 親コンポーネントで管理 | フック内で管理 | モーダル結果をPromiseで返す |
| アクセシビリティ | 自動対応(設定必要) | 自前実装 | 自前実装 |
| 複数モーダル | 状態を個別に管理 | フックを複数定義 | 関数を複数定義 |
| 向いている用途 | 複雑なUIモーダル | 再利用可能なモーダル群 | 確認/アラートダイアログ |
モーダルライブラリを選ぶ際は、「UIの複雑さ」と「結果の扱い方」の2軸で考えると良いでしょう。
react-modalreact-modal-hookreact-modal-promiseどのライブラリも一長一短ありますが、要件に合ったものを選べば、モーダル周りのコードがぐっとシンプルになります。
モーダルの表示ロジックを再利用可能なカスタムフックとして切り出したい場合に最適です。特に、複数のモーダルを同じ画面で使い分けるような複雑なUIにおいて、各モーダルの状態を独立して管理できます。ただし、モーダルコンテンツのレンダリングは開発者自身が行う必要があり、スタイルやフォーカス管理も自前で実装する必要があります。
既存のプロジェクトや、モーダルの外観・振る舞いを細かく制御したい場合に適しています。ただし、モーダルの表示状態やフォーカストラップなどのアクセシビリティ対応を自前で管理する必要があります。シンプルなUI構成で、状態管理をReduxやZustandなど外部ストアと統合したいケースにも向いています。
モーダルの結果をPromiseとして扱いたい場合(例:確認ダイアログで「はい」/「いいえ」の選択を非同期的に待つ)に非常に有効です。関数呼び出し一つでモーダルを開き、その戻り値としてユーザーのアクションを受け取れるため、手続き的なコードフローを自然に記述できます。ただし、複雑なモーダルUIや高度なカスタマイズには向きません。
Syntactic sugar for handling modal windows using React Hooks.
This library does not provide any UI, but instead offers a convenient way to render modal components defined elsewhere.
For a simple modal component check out react-modal, which works well with this library.
npm install --save react-modal-hook
Use ModalProvider to provide modal context for your application:
import React from "react";
import ReactDOM from "react-dom";
import { ModalProvider } from "react-modal-hook";
import App from "./App";
ReactDOM.render(
<ModalProvider>
<App />
</ModalProvider>,
document.getElementById("root")
);
Call useModal with the dialog component of your choice. Example using react-modal:
import React from "react";
import ReactModal from "react-modal";
import { useModal } from "react-modal-hook";
const App = () => {
const [showModal, hideModal] = useModal(() => (
<ReactModal isOpen>
<p>Modal content</p>
<button onClick={hideModal}>Hide modal</button>
</ReactModal>
));
return <button onClick={showModal}>Show modal</button>;
};
Second argument to useModals should contain an array of values referenced inside the modal:
const App = () => {
const [count, setCount] = useState(0);
const [showModal] = useModal(
() => (
<ReactModal isOpen>
<span>The count is {count}</span>
<button onClick={() => setCount(count + 1)}>Increment</button>
</ReactModal>
),
[count]
);
return <button onClick={showModal}>Show modal</button>;
};
Modals are also functional components and can use react hooks themselves:
const App = () => {
const [showModal] = useModal(() => {
const [count, setCount] = useState(0);
return (
<ReactModal isOpen>
<span>The count is {count}</span>
<button onClick={() => setCount(count + 1)}>Increment</button>
</ReactModal>
);
});
return <button onClick={showModal}>Show modal</button>;
};
Use TransitionGroup as the container for the modals:
import React from "react";
import ReactDOM from "react-dom";
import { ModalProvider } from "react-modal-hook";
import { TransitionGroup } from "react-transition-group";
import App from "./App";
ReactDOM.render(
<ModalProvider rootComponent={TransitionGroup}>
<App />
</ModalProvider>,
document.getElementById("root")
);
When TransitionGroup detects of one of its children removed, it will set its in prop to false and wait for onExited callback to be called before removing it from the DOM.
Those props are automatically added to all components passed to useModal. You can can pass them down to CSSTransition or modal component with transition support.
Here's an example using Material-UI's Dialog:
import React from "react";
import { useModal } from "react-modal-hook";
import { Button, Dialog, DialogActions, DialogTitle } from "@material-ui/core";
const App = () => {
const [showModal, hideModal] = useModal(({ in: open, onExited }) => (
<Dialog open={open} onExited={onExited} onClose={hideModal}>
<DialogTitle>Dialog Content</DialogTitle>
<DialogActions>
<Button onClick={hideModal}>Close</Button>
</DialogActions>
</Dialog>
));
return <Button onClick={showModal}>Show modal</Button>;
};
MIT © mpontus