1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

状態管理を使った “確認UI” のつくり方(モーダル・ダイアログ・トースト)

1
Posted at

React モーダルの基本的な使い方

React でモーダルを実装する基本は、次の 3 ステップです。

  1. モーダルの開閉状態を useState で管理する
  2. 開くボタンと閉じるボタンを用意する
  3. モーダルを条件付きレンダリングする

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」

ここまで見てきたように、

  • モーダル
  • 確認ダイアログ
  • トースト通知

はどれも、

  1. React の状態で「出すか・出さないか」を管理し
  2. isOpen && <Component />list.map()条件付きレンダリングし
  3. 必要に応じて Portal で最前面に描画する

という同じパターンで作れます。

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?