React Portalでモーダルとツールチップを簡単実装する方法

ReactでリッチなUIを実現するためには、モーダルやツールチップといった要素が欠かせません。しかし、これらの要素は通常のDOMツリー構造では予期しないレイアウトやスタイリングの問題を引き起こす可能性があります。こうした問題を解決するために、ReactにはPortalという便利な機能が用意されています。本記事では、React Portalを活用して、モーダルやツールチップを効果的に実装する方法を基礎から応用まで詳しく解説します。Reactでの開発をより効率化し、ユーザーにとって直感的で洗練されたUIを提供するためのヒントを学びましょう。

目次
  1. React Portalとは
    1. Portalの仕組み
    2. Portalのメリット
  2. Portalを使うべきシーン
    1. 1. モーダルダイアログ
    2. 2. ツールチップ
    3. 3. ドロップダウンメニュー
    4. 4. コンテキストメニュー
    5. 5. フルスクリーンのオーバーレイ
    6. 6. 独立したUI要素の管理
  3. モーダルの基本実装例
    1. 1. 必要な準備
    2. 2. モーダルコンポーネントの作成
    3. 3. モーダルをアプリケーションに統合
    4. 4. 実装のポイント
    5. 5. 実行結果
  4. モーダルの高度なカスタマイズ例
    1. 1. アニメーションの追加
    2. 2. 背景クリックでの閉じる動作
    3. 3. スクロールの制御
    4. 4. カスタムコンテンツと動的サイズ変更
    5. 5. 実行結果
  5. ツールチップの基本実装例
    1. 1. ツールチップの仕組み
    2. 2. ツールチップのHTMLとCSS
    3. 3. ツールチップコンポーネント
    4. 4. ツールチップの使用例
    5. 5. 実装のポイント
    6. 6. 実行結果
  6. ツールチップの応用例
    1. 1. 動的な位置調整
    2. 2. カスタムスタイルの適用
    3. 3. 複数ターゲットの対応
    4. 4. ツールチップのテーマ切り替え
    5. 5. 実行結果
  7. Portal使用時の注意点
    1. 1. SEOへの影響
    2. 2. アクセシビリティ対策
    3. 3. コンテンツのスタイルと位置
    4. 4. メモリリークの防止
    5. 5. イベントのバブルリング
    6. 6. 開発ツールでのデバッグ
    7. 7. テストの考慮
    8. まとめ
  8. Portalとライブラリ活用
    1. 1. React Transition Groupによるアニメーション
    2. 2. React Ariaによるアクセシビリティ対応
    3. 3. Material-UI (MUI)の利用
    4. 4. React-Popperによるツールチップの位置管理
    5. 5. 実装のメリット
    6. まとめ
  9. まとめ

React Portalとは


React Portalは、Reactアプリケーション内のコンポーネントを現在のDOMツリーとは別の場所にレンダリングできる機能です。通常、Reactコンポーネントはその親要素の中にDOMがレンダリングされますが、Portalを使用すると、アプリケーションルート外にDOMを生成することができます。

Portalの仕組み


Portalは、ReactDOM.createPortalメソッドを用いて作成されます。このメソッドは、2つの引数を取ります:

  1. React要素:レンダリングするコンテンツ。
  2. DOMノード:コンテンツを挿入するターゲットのDOMノード。

以下は基本的な構文です:

ReactDOM.createPortal(
  <div>Portal Content</div>,
  document.getElementById('portal-root')
);

Portalのメリット

  1. DOMツリーの分離
    モーダルやツールチップなど、通常のコンポーネントツリーに影響を与えずに別のDOM階層に配置できます。これにより、親要素のCSSによる影響を回避できます。
  2. Z-indexの管理が容易
    高度なUI構成を行う際、DOMツリー外で描画することで、Z-indexの衝突や複雑なスタイルの調整を避けられます。
  3. イベントのバブルリング
    Portalでレンダリングされたコンテンツも、Reactのイベントシステムを通じて親コンポーネントにイベントを伝播できます。これにより、既存のイベント処理コードを再利用可能です。

React Portalは、複雑なUI構成をシンプルにし、柔軟性を向上させる強力なツールです。この特性を活かすことで、よりモダンでメンテナンス性の高いコードを書くことができます。

Portalを使うべきシーン

Portalは、通常のDOMツリー内では解決が難しい問題を解消するために使用されます。以下は、Portalを活用すべき代表的なシーンです。

1. モーダルダイアログ


モーダルは、アプリケーション全体の視覚的な中心となるUIコンポーネントです。通常のDOMツリー内でレンダリングすると、親コンポーネントのスタイル(例えばoverflow: hiddenposition: relative)の影響を受けることがあります。Portalを使えば、モーダルをDOMのルート直下にレンダリングし、これらのスタイルの干渉を防ぐことができます。

2. ツールチップ


ツールチップは小さな情報表示用のコンポーネントであり、通常は他の要素の上に表示されます。Portalを使用すると、ツールチップをDOMツリーの外部に配置することで、他の要素のスタイルやスクロールの影響を受けず、位置の調整が容易になります。

3. ドロップダウンメニュー


ドロップダウンメニューも、Portalを使うことでレイアウトの問題を回避できます。親要素のoverflow: hiddenpositionによる切り捨てを防ぎ、どんなスクロール状態でも正しく表示されます。

4. コンテキストメニュー


右クリックなどで表示されるコンテキストメニューは、ページ全体に関わるUIであるため、Portalで別階層にレンダリングするのが適しています。これにより、他のUIコンポーネントやスタイルの影響を排除できます。

5. フルスクリーンのオーバーレイ


ローディングスピナーやポップアップアラートのような、アプリケーション全体に影響を及ぼすオーバーレイにもPortalが有効です。これにより、アプリケーションのルートに配置された要素として扱えるため、スタイルの衝突が発生しません。

6. 独立したUI要素の管理


特定のUI要素が他のコンポーネントから独立して管理される必要がある場合も、Portalが役立ちます。例えば、アニメーションのトリガーやイベントのスコープを管理する際に役立ちます。

React Portalを利用することで、DOMの構造を整理し、洗練されたUIを提供することが可能です。これにより、開発者の作業負担を減らし、ユーザー体験を向上させることができます。

モーダルの基本実装例

React Portalを使用したシンプルなモーダルの作り方を解説します。以下の例では、Portalの基本概念とともに、モーダルの表示・非表示を制御する方法を説明します。

1. 必要な準備


モーダルをPortalでレンダリングするには、まず、HTMLにモーダル専用のDOMノードを用意します。通常、public/index.htmlに次のような要素を追加します:

<div id="modal-root"></div>

このdiv要素がPortalのターゲットノードになります。

2. モーダルコンポーネントの作成


次に、モーダル用のReactコンポーネントを作成します。Portalを使ってmodal-rootにモーダルをレンダリングします。

import React from "react";
import ReactDOM from "react-dom";

const Modal = ({ isOpen, onClose, children }) => {
  if (!isOpen) return null;

  return ReactDOM.createPortal(
    <div style={styles.overlay}>
      <div style={styles.modal}>
        <button onClick={onClose} style={styles.closeButton}>×</button>
        {children}
      </div>
    </div>,
    document.getElementById("modal-root")
  );
};

const styles = {
  overlay: {
    position: "fixed",
    top: 0,
    left: 0,
    width: "100%",
    height: "100%",
    backgroundColor: "rgba(0, 0, 0, 0.5)",
    display: "flex",
    justifyContent: "center",
    alignItems: "center"
  },
  modal: {
    background: "white",
    padding: "20px",
    borderRadius: "8px",
    maxWidth: "500px",
    width: "100%"
  },
  closeButton: {
    position: "absolute",
    top: "10px",
    right: "10px",
    background: "transparent",
    border: "none",
    fontSize: "18px",
    cursor: "pointer"
  }
};

export default Modal;

3. モーダルをアプリケーションに統合


作成したモーダルをアプリケーションに統合し、表示・非表示を制御します。

import React, { useState } from "react";
import Modal from "./Modal";

const App = () => {
  const [isModalOpen, setModalOpen] = useState(false);

  return (
    <div>
      <h1>React Portalでモーダルを実装</h1>
      <button onClick={() => setModalOpen(true)}>モーダルを開く</button>
      <Modal isOpen={isModalOpen} onClose={() => setModalOpen(false)}>
        <h2>これはモーダルです</h2>
        <p>React Portalを使って簡単に実装できます。</p>
      </Modal>
    </div>
  );
};

export default App;

4. 実装のポイント

  • 条件付きレンダリングisOpenプロップでモーダルの表示状態を管理します。
  • イベントハンドリング:モーダルを閉じるためにonCloseプロップを使用します。
  • スタイリング:モーダルを適切に配置するためのCSSスタイルを指定します。

5. 実行結果


アプリケーションを実行すると、「モーダルを開く」ボタンをクリックして、中央に表示されるモーダルを開閉できます。このようにPortalを使用すると、簡単かつ柔軟にモーダルを実装できます。

モーダルの高度なカスタマイズ例

基本的なモーダル実装に加え、よりリッチなユーザー体験を提供するためのカスタマイズ方法を紹介します。ここでは、アニメーションの追加、背景クリックでの閉じる動作、スクロールの制御などを実現します。

1. アニメーションの追加


モーダルにアニメーションを付けることで、より洗練されたUIを提供できます。以下は、CSSトランジションを使ったアニメーションの例です。

import React from "react";
import ReactDOM from "react-dom";
import "./Modal.css"; // CSSファイルをインポート

const Modal = ({ isOpen, onClose, children }) => {
  if (!isOpen) return null;

  return ReactDOM.createPortal(
    <div className={`overlay ${isOpen ? "fade-in" : "fade-out"}`}>
      <div className="modal">
        <button onClick={onClose} className="close-button">×</button>
        {children}
      </div>
    </div>,
    document.getElementById("modal-root")
  );
};

export default Modal;
/* Modal.css */
.overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}

.overlay.fade-in {
  opacity: 1;
  visibility: visible;
}

.modal {
  background: white;
  padding: 20px;
  border-radius: 8px;
  max-width: 500px;
  width: 100%;
  animation: slide-in 0.3s ease;
}

@keyframes slide-in {
  from {
    transform: translateY(-20%);
  }
  to {
    transform: translateY(0);
  }
}

2. 背景クリックでの閉じる動作


モーダルの外側(背景)をクリックすると閉じるように設定します。

const Modal = ({ isOpen, onClose, children }) => {
  if (!isOpen) return null;

  const handleOverlayClick = (e) => {
    if (e.target === e.currentTarget) onClose();
  };

  return ReactDOM.createPortal(
    <div className={`overlay ${isOpen ? "fade-in" : "fade-out"}`} onClick={handleOverlayClick}>
      <div className="modal">
        <button onClick={onClose} className="close-button">×</button>
        {children}
      </div>
    </div>,
    document.getElementById("modal-root")
  );
};

3. スクロールの制御


モーダルが開いている間、背景のスクロールを無効化することで、ユーザー体験を向上させます。

import { useEffect } from "react";

const Modal = ({ isOpen, onClose, children }) => {
  useEffect(() => {
    if (isOpen) {
      document.body.style.overflow = "hidden";
    } else {
      document.body.style.overflow = "auto";
    }
    return () => {
      document.body.style.overflow = "auto";
    };
  }, [isOpen]);

  if (!isOpen) return null;

  return ReactDOM.createPortal(
    <div className={`overlay ${isOpen ? "fade-in" : "fade-out"}`}>
      <div className="modal">
        <button onClick={onClose} className="close-button">×</button>
        {children}
      </div>
    </div>,
    document.getElementById("modal-root")
  );
};

4. カスタムコンテンツと動的サイズ変更


モーダル内のコンテンツが動的に変化する場合、レスポンシブデザインを組み込むことで、どんな画面サイズでも快適に使用できます。

.modal {
  max-width: 80%;
  max-height: 80%;
  overflow: auto;
}

5. 実行結果


これらのカスタマイズにより、モーダルは以下の機能を持つようになります:

  • アニメーションによるスムーズな開閉。
  • 背景クリックでモーダルを閉じる。
  • モーダル表示中の背景スクロール制御。
  • レスポンシブなデザイン対応。

高度なカスタマイズを行うことで、より洗練されたモーダルを実現し、ユーザー体験を向上させることが可能です。

ツールチップの基本実装例

React Portalを活用することで、ツールチップを簡単かつ効果的に実装できます。ここでは、基本的なツールチップの作成方法を解説します。

1. ツールチップの仕組み


ツールチップは、特定の要素にマウスオーバーした際に追加情報を表示するための小さなポップアップです。Portalを使用することで、ツールチップをDOMツリー外にレンダリングし、親コンポーネントやスタイルの影響を受けない設計にできます。

2. ツールチップのHTMLとCSS


ツールチップのデザインとスタイルを以下のように定義します。

/* Tooltip.css */
.tooltip {
  position: fixed;
  background-color: #333;
  color: #fff;
  padding: 8px;
  border-radius: 4px;
  font-size: 14px;
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.2s;
}

.tooltip.visible {
  visibility: visible;
  opacity: 1;
}

3. ツールチップコンポーネント


ツールチップをReactコンポーネントとして定義し、Portalを使用してDOMに追加します。

import React, { useState, useRef, useEffect } from "react";
import ReactDOM from "react-dom";
import "./Tooltip.css";

const Tooltip = ({ text, targetRef }) => {
  const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
  const tooltipRef = useRef(null);

  useEffect(() => {
    if (targetRef.current && tooltipRef.current) {
      const targetRect = targetRef.current.getBoundingClientRect();
      const tooltipRect = tooltipRef.current.getBoundingClientRect();

      setTooltipPosition({
        top: targetRect.top - tooltipRect.height - 8,
        left: targetRect.left + targetRect.width / 2 - tooltipRect.width / 2
      });
    }
  }, [targetRef]);

  return ReactDOM.createPortal(
    <div
      ref={tooltipRef}
      className="tooltip visible"
      style={{ top: `${tooltipPosition.top}px`, left: `${tooltipPosition.left}px` }}
    >
      {text}
    </div>,
    document.body
  );
};

export default Tooltip;

4. ツールチップの使用例


このツールチップを使うには、ターゲットとなる要素を参照するrefを渡します。

import React, { useState, useRef } from "react";
import Tooltip from "./Tooltip";

const App = () => {
  const [showTooltip, setShowTooltip] = useState(false);
  const targetRef = useRef(null);

  return (
    <div style={{ padding: "50px" }}>
      <button
        ref={targetRef}
        onMouseEnter={() => setShowTooltip(true)}
        onMouseLeave={() => setShowTooltip(false)}
      >
        ツールチップを表示
      </button>
      {showTooltip && <Tooltip text="ここがツールチップです" targetRef={targetRef} />}
    </div>
  );
};

export default App;

5. 実装のポイント

  1. 位置の計算
    ツールチップの位置は、ターゲット要素の位置情報を取得して動的に計算します。getBoundingClientRectを使用して、画面上の正確な座標を取得します。
  2. Portalの活用
    ツールチップをdocument.bodyに直接レンダリングすることで、スタイルやレイアウトの干渉を回避します。
  3. イベントハンドリング
    マウスイベントでツールチップの表示/非表示を制御します。onMouseEnteronMouseLeaveを使い、簡単に切り替えられます。

6. 実行結果


アプリケーションを実行すると、ボタンにマウスを合わせた際にツールチップが表示され、画面上に正確に配置されます。このように、React Portalを使用することで、柔軟でスタイリッシュなツールチップを簡単に実装できます。

ツールチップの応用例

基本的なツールチップ実装をさらに強化し、動的な位置調整やカスタムスタイルを適用した応用例を紹介します。これにより、複雑なUIにも対応可能な柔軟なツールチップを作成できます。

1. 動的な位置調整


ターゲット要素が画面の端に配置されている場合、ツールチップが画面外に出てしまう可能性があります。この問題を解決するために、画面の境界を考慮した位置調整を行います。

const Tooltip = ({ text, targetRef }) => {
  const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
  const tooltipRef = useRef(null);

  useEffect(() => {
    if (targetRef.current && tooltipRef.current) {
      const targetRect = targetRef.current.getBoundingClientRect();
      const tooltipRect = tooltipRef.current.getBoundingClientRect();
      const padding = 8;

      let top = targetRect.top - tooltipRect.height - padding;
      let left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2;

      // Adjust for screen boundaries
      if (top < 0) {
        top = targetRect.bottom + padding;
      }
      if (left < 0) {
        left = padding;
      } else if (left + tooltipRect.width > window.innerWidth) {
        left = window.innerWidth - tooltipRect.width - padding;
      }

      setTooltipPosition({ top, left });
    }
  }, [targetRef]);

  return ReactDOM.createPortal(
    <div
      ref={tooltipRef}
      className="tooltip visible"
      style={{ top: `${tooltipPosition.top}px`, left: `${tooltipPosition.left}px` }}
    >
      {text}
    </div>,
    document.body
  );
};

2. カスタムスタイルの適用


ツールチップのデザインをカスタマイズし、用途に応じた外観を作成します。

/* Custom Tooltip Styles */
.tooltip {
  position: fixed;
  background-color: #222;
  color: #fff;
  padding: 10px 15px;
  border-radius: 6px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  font-size: 14px;
  font-family: Arial, sans-serif;
  z-index: 1000;
}

.tooltip::after {
  content: '';
  position: absolute;
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 6px;
  border-color: transparent transparent #222 transparent;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
}

3. 複数ターゲットの対応


ツールチップを複数の要素に対応させるには、ターゲット要素のidrefを動的に設定します。

const App = () => {
  const [tooltipData, setTooltipData] = useState({ visible: false, text: "", targetRef: null });

  const handleMouseEnter = (e, text) => {
    setTooltipData({ visible: true, text, targetRef: e.target });
  };

  const handleMouseLeave = () => {
    setTooltipData({ visible: false, text: "", targetRef: null });
  };

  return (
    <div style={{ padding: "50px" }}>
      <button
        onMouseEnter={(e) => handleMouseEnter(e, "ツールチップ1")}
        onMouseLeave={handleMouseLeave}
      >
        ボタン1
      </button>
      <button
        onMouseEnter={(e) => handleMouseEnter(e, "ツールチップ2")}
        onMouseLeave={handleMouseLeave}
      >
        ボタン2
      </button>
      {tooltipData.visible && (
        <Tooltip text={tooltipData.text} targetRef={{ current: tooltipData.targetRef }} />
      )}
    </div>
  );
};

4. ツールチップのテーマ切り替え


ツールチップにテーマ(ライトモードとダークモード)を設定し、状況に応じたスタイルを動的に適用します。

const Tooltip = ({ text, targetRef, theme = "dark" }) => {
  const themeClass = theme === "dark" ? "tooltip-dark" : "tooltip-light";

  return ReactDOM.createPortal(
    <div className={`tooltip ${themeClass}`} style={...}>
      {text}
    </div>,
    document.body
  );
};
/* Theme Styles */
.tooltip-dark {
  background-color: #333;
  color: #fff;
}

.tooltip-light {
  background-color: #fff;
  color: #333;
  border: 1px solid #ddd;
}

5. 実行結果


応用例を取り入れたツールチップは以下のような機能を持ちます:

  • 画面の境界を考慮した動的な位置調整。
  • カスタマイズ可能なスタイルとテーマ。
  • 複数のターゲット要素に対応。
  • スマートなデザインで高い視認性。

これらの工夫により、柔軟で実用的なツールチップを実装できます。UI全体の完成度を高める効果的なアプローチです。

Portal使用時の注意点

React Portalを活用する際には、便利さの反面、いくつかの注意点があります。これらのポイントを理解し、適切に対処することで、アプリケーションの品質を向上させることができます。

1. SEOへの影響


Portalでレンダリングされたコンテンツは、document.bodyの下に直接配置されるため、通常のDOMツリー構造とは異なります。これにより、以下のようなSEOの問題が発生する可能性があります。

  • 検索エンジンのインデックス化: 検索エンジンがPortal内のコンテンツを適切に読み取れない場合があります。特にモーダルに重要な情報を含める場合には注意が必要です。
  • サーバーサイドレンダリング: サーバーサイドでのレンダリング結果がSEOに影響する場合、Portal内のコンテンツもSSRで適切に処理する必要があります。

2. アクセシビリティ対策


Portalを使うと、DOMツリーの一貫性が失われるため、スクリーンリーダーやキーボードナビゲーションに影響を与える可能性があります。

  • 適切なラベル付け: モーダルやツールチップにはaria-labelaria-labelledbyを設定して、スクリーンリーダーがコンテンツを理解できるようにします。
  • フォーカストラップの実装: モーダルを使用する場合、フォーカスがモーダル内に閉じ込められるように制御する必要があります。
const trapFocus = (e) => {
  const focusableElements = modalRef.current.querySelectorAll("button, a, input, textarea, select");
  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];

  if (e.key === "Tab") {
    if (e.shiftKey && document.activeElement === firstElement) {
      e.preventDefault();
      lastElement.focus();
    } else if (!e.shiftKey && document.activeElement === lastElement) {
      e.preventDefault();
      firstElement.focus();
    }
  }
};

3. コンテンツのスタイルと位置


Portalでレンダリングされたコンテンツは、通常のDOMツリー外に配置されるため、スタイリングが想定通りにならない場合があります。

  • グローバルCSSの影響: Portal内のコンテンツには、ルートDOMに適用されているCSSがそのまま適用されることがあります。そのため、スタイルの衝突を避けるために適切なスコープ管理が必要です。
  • レスポンシブ対応: Portal内のコンテンツが画面サイズに応じて正しく位置調整されるように、動的な計算を導入する必要があります。

4. メモリリークの防止


Portalを使用して動的に生成されたコンテンツが、不要になった後もメモリに残り続ける場合があります。以下のような対策を講じましょう。

  • クリーンアップ処理: Portal内のイベントリスナーやタイマーなどは、コンポーネントのアンマウント時に必ず解除します。
  • ReactのStrictMode対応: 開発環境でStrictModeを有効にして、メモリリークや未処理の副作用をチェックします。

5. イベントのバブルリング


Portalでレンダリングされたコンテンツでも、Reactのイベントシステムによってバブルリングが発生します。これにより、親コンポーネントのイベントハンドラが意図せず実行される場合があります。

  • イベントの停止: 必要に応じて、e.stopPropagation()を使用してバブルリングを制御します。
const handleClick = (e) => {
  e.stopPropagation();
  console.log("Modal clicked!");
};

6. 開発ツールでのデバッグ


Portalを使用したコンポーネントは通常のDOMツリー内に表示されないため、開発ツールでのデバッグが難しくなる場合があります。

  • React Developer Toolsの活用: React Developer Toolsを使用して、Portal内のコンポーネントを確認します。
  • コンソールログの活用: 必要な情報をコンソールログに出力して、デバッグを効率化します。

7. テストの考慮


Portalを利用したコンポーネントのテストでは、レンダリング先のDOMノードが適切に存在することを確認する必要があります。

  • テスト用DOMノードの用意: テスト環境でPortalを適切に動作させるために、モックDOMノードを準備します。
  • エンドツーエンドテスト: ユーザー視点での動作確認を行うためにE2Eテストを活用します。

まとめ


React Portalを使用する際には、SEOやアクセシビリティ、スタイルの影響、デバッグやテストの難しさなど、いくつかの課題があります。これらの注意点を事前に把握し、適切に対策を講じることで、洗練されたUIを実現しながら、品質の高いアプリケーションを開発できます。

Portalとライブラリ活用

React Portalを使用すると、独自のモーダルやツールチップを作成できますが、さらに効率的な開発を目指す場合には、Reactのエコシステムに存在するライブラリを活用するのが有効です。本章では、Portalと組み合わせて利用できるライブラリをいくつか紹介し、その使用方法を解説します。

1. React Transition Groupによるアニメーション

React Transition Groupを使用すると、Portalでレンダリングされたモーダルやツールチップにアニメーションを簡単に追加できます。

npm install react-transition-group

以下はモーダルにアニメーションを追加する例です。

import React from "react";
import ReactDOM from "react-dom";
import { CSSTransition } from "react-transition-group";
import "./Modal.css";

const Modal = ({ isOpen, onClose, children }) => {
  return ReactDOM.createPortal(
    <CSSTransition
      in={isOpen}
      timeout={300}
      classNames="modal"
      unmountOnExit
    >
      <div className="overlay" onClick={onClose}>
        <div className="modal" onClick={(e) => e.stopPropagation()}>
          {children}
        </div>
      </div>
    </CSSTransition>,
    document.getElementById("modal-root")
  );
};

export default Modal;

CSSアニメーションの設定例:

/* Modal.css */
.modal-enter {
  opacity: 0;
  transform: scale(0.9);
}
.modal-enter-active {
  opacity: 1;
  transform: scale(1);
  transition: opacity 300ms, transform 300ms;
}
.modal-exit {
  opacity: 1;
  transform: scale(1);
}
.modal-exit-active {
  opacity: 0;
  transform: scale(0.9);
  transition: opacity 300ms, transform 300ms;
}

2. React Ariaによるアクセシビリティ対応

React Ariaは、アクセシビリティを考慮したUIコンポーネントを作成するためのライブラリです。Portalを使ったモーダルやツールチップにも適用可能です。

npm install @react-aria/overlays

以下はReact Ariaを使ったモーダルの例です:

import React from "react";
import ReactDOM from "react-dom";
import { useOverlay, usePreventScroll, useModal } from "@react-aria/overlays";

const Modal = ({ isOpen, onClose, children }) => {
  let ref = React.useRef();
  let { overlayProps } = useOverlay(
    { isOpen, onClose, isDismissable: true },
    ref
  );
  usePreventScroll();
  useModal();

  return isOpen
    ? ReactDOM.createPortal(
        <div {...overlayProps} ref={ref} className="modal">
          {children}
        </div>,
        document.getElementById("modal-root")
      )
    : null;
};

export default Modal;

3. Material-UI (MUI)の利用

Material-UI(MUI)は、多機能なReactコンポーネントライブラリで、Portalを使用した高度なUI要素が含まれています。

npm install @mui/material @emotion/react @emotion/styled

以下はMUIのModalコンポーネントを使用した例です:

import React, { useState } from "react";
import Modal from "@mui/material/Modal";
import Box from "@mui/material/Box";

const style = {
  position: "absolute",
  top: "50%",
  left: "50%",
  transform: "translate(-50%, -50%)",
  width: 400,
  bgcolor: "background.paper",
  border: "2px solid #000",
  boxShadow: 24,
  p: 4,
};

const App = () => {
  const [open, setOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setOpen(true)}>モーダルを開く</button>
      <Modal open={open} onClose={() => setOpen(false)}>
        <Box sx={style}>
          <h2>Material-UI モーダル</h2>
          <p>簡単に利用可能なPortalベースのモーダルです。</p>
        </Box>
      </Modal>
    </div>
  );
};

export default App;

4. React-Popperによるツールチップの位置管理

React Popperを使用すると、ツールチップやドロップダウンの位置を動的に管理できます。

npm install @popperjs/core react-popper

以下はツールチップの例です:

import React from "react";
import { usePopper } from "react-popper";

const Tooltip = ({ text, referenceElement }) => {
  const [popperElement, setPopperElement] = React.useState(null);
  const { styles, attributes } = usePopper(referenceElement, popperElement);

  return (
    <div ref={setPopperElement} style={styles.popper} {...attributes.popper}>
      {text}
    </div>
  );
};

5. 実装のメリット

  • React Transition Group: アニメーションを簡単に実現。
  • React Aria: アクセシビリティに対応。
  • Material-UI: デザインと機能性のバランスが良い。
  • React Popper: ツールチップやドロップダウンの位置調整が容易。

まとめ


Portalの柔軟性を活かしつつ、Reactのライブラリを活用することで、開発効率が向上し、高品質なUIを実現できます。各ライブラリの特性を理解し、プロジェクトの要件に応じて選択しましょう。

まとめ

本記事では、React Portalを使用してモーダルやツールチップを実装する方法を、基本から応用まで詳しく解説しました。React Portalを利用することで、DOMツリー外でのコンテンツのレンダリングが可能になり、スタイリングやレイアウトの問題を避けることができます。

特に、モーダルやツールチップの実装において、Portalは非常に便利であり、ユーザーインターフェースの品質を高めるための強力なツールです。また、React Transition GroupやReact Aria、Material-UI、React Popperなど、ライブラリを活用することで、アニメーション、アクセシビリティ、位置管理といった高度な機能を簡単に実装できます。

Portalを利用する際の注意点、例えばSEOやアクセシビリティの対応、メモリリークの防止、イベントのバブルリング制御についても解説しました。これらのポイントをしっかりと押さえておくことで、より品質の高い、メンテナンス性のあるReactアプリケーションを構築することができます。

React Portalとその関連技術を活用することで、よりリッチでダイナミックなUIを実現し、ユーザー体験を向上させることができます。

コメント

コメントする

目次
  1. React Portalとは
    1. Portalの仕組み
    2. Portalのメリット
  2. Portalを使うべきシーン
    1. 1. モーダルダイアログ
    2. 2. ツールチップ
    3. 3. ドロップダウンメニュー
    4. 4. コンテキストメニュー
    5. 5. フルスクリーンのオーバーレイ
    6. 6. 独立したUI要素の管理
  3. モーダルの基本実装例
    1. 1. 必要な準備
    2. 2. モーダルコンポーネントの作成
    3. 3. モーダルをアプリケーションに統合
    4. 4. 実装のポイント
    5. 5. 実行結果
  4. モーダルの高度なカスタマイズ例
    1. 1. アニメーションの追加
    2. 2. 背景クリックでの閉じる動作
    3. 3. スクロールの制御
    4. 4. カスタムコンテンツと動的サイズ変更
    5. 5. 実行結果
  5. ツールチップの基本実装例
    1. 1. ツールチップの仕組み
    2. 2. ツールチップのHTMLとCSS
    3. 3. ツールチップコンポーネント
    4. 4. ツールチップの使用例
    5. 5. 実装のポイント
    6. 6. 実行結果
  6. ツールチップの応用例
    1. 1. 動的な位置調整
    2. 2. カスタムスタイルの適用
    3. 3. 複数ターゲットの対応
    4. 4. ツールチップのテーマ切り替え
    5. 5. 実行結果
  7. Portal使用時の注意点
    1. 1. SEOへの影響
    2. 2. アクセシビリティ対策
    3. 3. コンテンツのスタイルと位置
    4. 4. メモリリークの防止
    5. 5. イベントのバブルリング
    6. 6. 開発ツールでのデバッグ
    7. 7. テストの考慮
    8. まとめ
  8. Portalとライブラリ活用
    1. 1. React Transition Groupによるアニメーション
    2. 2. React Ariaによるアクセシビリティ対応
    3. 3. Material-UI (MUI)の利用
    4. 4. React-Popperによるツールチップの位置管理
    5. 5. 実装のメリット
    6. まとめ
  9. まとめ