赤いエラー画面: 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 ノードが必要な場合を除き、コンポーネントは柔軟にしておきましょう。

