React モーダルの基本的な使い方
React でモーダルを実装する基本は、次の 3 ステップです。
- モーダルの開閉状態を useState で管理する
- 開くボタンと閉じるボタンを用意する
- モーダルを条件付きレンダリングする
1. モーダルの開閉状態を管理する
まずは「モーダルを表示するかどうか」を useState で管理します。
const [isOpen, setIsOpen] = useState(false);
-
isOpen === true→ モーダルを表示 -
isOpen === false→ モーダルを非表示
React のモーダルは、“状態の切り替え” が本体です。
2. ボタンから開閉を操作する
<button onClick={() => setIsOpen(true)}>モーダルを開く</button>
モーダル内では閉じるボタンを用意。
<button onClick={() => setIsOpen(false)}>閉じる</button>
3. モーダル本体を条件付きで表示する
最もシンプルなモーダルの構造はこんな感じです
{isOpen && (
<div className="overlay">
<div className="modal">
<p>モーダルの内容</p>
<button onClick={() => setIsOpen(false)}>閉じる</button>
</div>
</div>
)}
最低限必要な CSS
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
}
.modal {
background: white;
padding: 24px;
width: 300px;
margin: 100px auto;
border-radius: 8px;
}
これだけで最低限のモーダルとして成立します。
React らしい実装にするなら「Portal」が必須
Portal を使う最大の理由は「モーダルがレイアウトや z-index の影響を受けず、必ず最前面に正しく表示されるから」。
逆に使わないと、スクロール・z-index・overflow の影響を受けてモーダルが“壊れやすい UI”になる。
モーダルは画面の最前面に置くのが前提ですが、
コンポーネントのツリー内にそのまま描画すると、
- 親要素に z-index が設定されている
- 祖先要素が
position: relativeを持っている - flex や grid のスタッキングコンテキストの影響を受ける
などによって 意図せず後ろに隠れる ことがあります。
Portal は document.body 直下に描画されるので、
レイアウトツリーの z-index の干渉を受けない。
Portal を使った例
import ReactDOM from "react-dom";
const Modal = ({ children, onClose }) => {
return ReactDOM.createPortal(
<div className="overlay" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.body
);
};
これで「親コンポーネントの DOM 外にレンダリング」できるので、
- レイアウトが崩れにくい
- スクロールの影響を受けない
- z-index 地獄を避けやすい
といったメリットがあります。
モーダルが分かれば、ダイアログとトーストも同じ考え方で作れる
React でモーダルを実装するときのポイントは、
-
isOpenなどの 状態で表示・非表示を管理する -
isOpen && <Modal />のように 条件付きレンダリングする - 必要に応じて Portal で最前面に出す
この考え方は、確認ダイアログやトースト通知にもそのまま応用できます。
「見た目や振る舞いは違うけれど、やっていることは全部“状態に応じてコンポーネントを出したり消したりしているだけ”」です。
1. ダイアログ:モーダルの“用途違い”として実装できる
ダイアログは、
- 「本当に削除しますか?」のような 短い確認メッセージ
- 「OK / キャンセル」などの 選択肢ボタン
を持つ、シンプルな用途に特化したモーダルと捉えられます。
実装の型はモーダルと同じで、違うのは「中身」と「戻り値の扱い」だけです。
type ConfirmDialogProps = {
message: string;
onConfirm: () => void;
onCancel: () => void;
};
const ConfirmDialog = ({ message, onConfirm, onCancel }: ConfirmDialogProps) => {
return ReactDOM.createPortal(
<div className="overlay" onClick={onCancel}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<p>{message}</p>
<button onClick={onConfirm}>OK</button>
<button onClick={onCancel}>キャンセル</button>
</div>
</div>,
document.body
);
};
呼び出し側は、モーダルと同じく isOpen で制御します。
const [isOpen, setIsOpen] = useState(false);
const handleDelete = () => {
setIsOpen(true);
};
const handleConfirm = () => {
// 削除処理など
setIsOpen(false);
};
const handleCancel = () => {
setIsOpen(false);
};
ポイント
- モーダルと同じ「状態+条件付きレンダリング+Portal」の形
- 「OK なら実行 / キャンセルなら何もしない」という 分岐の中身だけが違う
2. トースト:位置とライフサイクルが違うだけの“軽量モーダル”
トースト通知は、
- 画面の端(右上など)に数秒だけ表示される
- 背景はブロックしない(画面操作は継続できる)
- メッセージ中心で、ボタンはあっても 1 つ程度
という、さらに軽量な一時表示コンポーネントです。
ここでもやることは同じで、
-
toastsという配列 or 単一のtoast状態を持つ - ある条件で 追加する
- 数秒後に 自動で削除する
-
toasts.map()で画面右上などに描画する
だけです。
type Toast = {
id: number;
message: string;
};
const ToastContainer = ({ toasts }: { toasts: Toast[] }) => {
return ReactDOM.createPortal(
<div className="toast-container">
{toasts.map((t) => (
<div key={t.id} className="toast">
{t.message}
</div>
))}
</div>,
document.body
);
};
呼び出し側では、通知を追加してタイマーで消すだけです。
const [toasts, setToasts] = useState<Toast[]>([]);
const showToast = (message: string) => {
const id = Date.now();
setToasts((prev) => [...prev, { id, message }]);
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 3000);
};
ポイント
- 「状態を配列で持つ」「数秒後に削除する」というライフサイクルの違いだけで、本質はやはり「状態に応じてコンポーネントを出し入れしているだけ」
- Portal を使って
body直下に出せば、レイアウトに影響されず右上固定などにしやすい
3. 共通するのは「状態 + 条件付きレンダリング + (必要なら)Portal」
ここまで見てきたように、
- モーダル
- 確認ダイアログ
- トースト通知
はどれも、
- React の状態で「出すか・出さないか」を管理し
-
isOpen && <Component />やlist.map()で 条件付きレンダリングし - 必要に応じて Portal で最前面に描画する
という同じパターンで作れます。