ラッパーコンポーネントにおける「React.Children.only」エラーの解決方法

intermediate⚛️ React2026-06-10| React (Web/Native) 全バージョン。通常、開発中に React.Children.only や React.cloneElement などのユーティリティ関数を子ノードのコレクションに対して使用した際に発生します。

Error Message

React.Children.only expected to receive a single React element child.
#react#javascript#フロントエンド開発#デバッグ

赤いエラー画面: React.Children.onlyおそらく、真っ赤なエラー画面(オーバーレイ)が表示されていることでしょう。これはUIライブラリやカスタムラッパーを作成する際によく遭遇する苛立たしい問題です。メッセージは非常に簡潔です。

Error: React.Children.only expected to receive a single React element child.

これは、Tooltip、Portal、Context Providerなどのコンポーネントが、正確に1つの子要素を期待しているにもかかわらず、それ以外のものが渡された場合に発生します。ソースコードのどこかで、コンポーネントが React.Children.only(children) を呼び出しています。もし children プロップに要素が含まれていないか、あるいは2つ以上の要素が含まれている場合、Reactは即座に例外をスローします。

なぜReactはこれほど厳格なのかReact.Children.only() ユーティリティは、ゲートキーパー(門番)のような役割を果たします。特定の ref を注入したり、固有の CSS スタイルを適用したりするなど、単一のノードに対してのみ意味を成す操作を行う際に、コンポーネントが壊れないことを保証します。ボタンの隣に単純な文字列を置くだけでも、このエラーは発生します。Reactはそれを、テキストノードと要素という2つの異なる子要素として認識するためです。

シナリオ:クラッシュした Tooltipカスタムの Tooltip ラッパーがあるとしましょう。これは単一のボタンをラップし、ホバー時にラベルを表示するように設計されています。以下がクラッシュの原因となるロジックです。

const Tooltip = ({ children, text }) => {
  // この行が唯一の失敗箇所です
  const child = React.Children.only(children);

  return (
    <div className="tooltip-container">
      {child}
      <span className="tooltip-text">{text}</span>
    </div>
  );
};

これは、単一の要素に対しては完璧に動作します。

<Tooltip text="保存">
  <button>送信</button>
</Tooltip>

しかし、アイコンや2つ目のテキストを追加した瞬間にクラッシュします。

<Tooltip text="保存">
  <span>💾</span>
  <button>送信</button>
</Tooltip>

解決方法### 1. クイックフィックス:コンテナでラップする最も手っ取り早い解決策は、子要素を単一の <div><span> でまとめることです。これにより、単一の親要素が必要であるという条件が満たされます。実装には10秒もかかりません。

<Tooltip text="保存">
  <div>
    <span>💾</span>
    <button>送信</button>
  </div>
</Tooltip>

プロのヒント: この方法には注意が必要です。親コンポーネントが CSS ルールで直接の子要素を期待している場合、余分な <div> を追加すると Flexbox や Grid レイアウトが崩れることがあります。

2. 柔軟な方法:React.Children.mapもしあなたがラッパーコンポーネントを作成している側であれば、より堅牢な作りにすることができます。単一の子要素を強制する代わりに、子要素を反復処理します。これにより、コンポーネントが1つ、2つ、あるいは10個の子要素を適切に処理できるようになります。

const Tooltip = ({ children, text }) => {
  return (
    <div className="tooltip-container">
      {React.Children.map(children, (child) => {
        // クローンを作成する前に、有効な要素であるか確認します
        return React.isValidElement(child) ? React.cloneElement(child) : child;
      })}
      <span className="tooltip-text">{text}</span>
    </div>
  );
};

3. Fragment の罠Fragment(<>...</>)を使って解決しようとするかもしれません。Fragment は技術的には単一の子要素としてカウントされますが、別の問題を引き起こす可能性があります。もしコンポーネントが React.cloneElement を使用してプロップを渡している場合、それらのプロップは Fragment 内の要素ではなく、Fragment 自体に付与されてしまいます。Fragment は DOM に存在しないため、スタイルやイベントリスナーが単純に消失してしまいます。

検証とテスト

  • コンソールを確認する: ブラウザを更新します。赤いエラーが消え、UIがレンダリングされているはずです。
  • DOMを検証する: デベロッパーツール(F12)を使用して、オプション1で追加した余分な <div> がレイアウトの間隔に影響を与えていないか確認します。
  • エッジケースを試す: コンポーネントにプレーンな文字列や null 値を渡してみます。本当に堅牢なラッパーであれば、children が空であってもクラッシュすべきではありません。

より優れた防御的コーディング

  • PropTypes を定義する: children: PropTypes.element.isRequired を使用します。これにより、アプリが実際にクラッシュする前に、開発中のコンソールに明確な警告が表示されます。
  • バリデーション: cloneElement の呼び出しは、必ず React.isValidElement() チェックの中に含めてください。これにより、ユーザーが文字列や数値を渡した際のクラッシュを防ぐことができます。
  • 可能な限り「only」を避ける: 位置計算のために厳密に単一の DOM ノードが必要な場合を除き、コンポーネントは柔軟にしておきましょう。

Related Error Notes