こんにちは。フロントエンドの現場で日々コンポーネントの海と格闘している皆さん、調子はどうですか?
コードレビューをしていて、よくこんな型定義を見かけませんか?
// よくある「とりあえず ReactNode にしとけ」パターン
type MyButtonProps = {
children: React.ReactNode;
};
「動くからこれでいいか」とスルーしがちですが、中級から一歩抜け出して「真のシニア」を目指すなら、ここにある2つの巨頭、`React.ReactNode` と `React.ReactElement` の違いを正確に言語化できなければなりません。
今日は、この2つの型がReactの内部やブラウザの裏側でどう扱われているのか、そして実務の現場でどう使い分けるべきか、泥臭い実例を交えて徹底的に解説していきます。
—
1. そもそもこの2つは何が違うのか?(概念の整理)
一言で言うと、「許容する範囲の広さ」が全く違います。
- `React.ReactNode`: 宇宙です。何でも入ります。JSX、文字列、数値、`null`、`undefined`、果てはこれらを詰め込んだ配列まで全部飲み込みます。
- `React.ReactElement`: 厳格な貴族です。「JSX構文から生まれた純粋なReact要素のオブジェクト(`{ type, props, key… }`)」しか受け付けません。文字列や数値は「ただのプリミティブ値だろ?」と門前払いを食らいます。
TypeScriptの型定義をのぞいてみると、その差は一目瞭然です。
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
// ReactNode はこれに null, undefined, boolean, ポータル, 配列などが無限に合体したカオス(褒め言葉)です
—
2. 裏側で何が起きているか?(Reactのレンダリングとブラウザの視点)
では、Reactやブラウザは裏側でこれらをどう処理しているのでしょうか?
私たちが書いた JSX(例: `
`)は、Babelなどのトランスパイラによって `React.createElement(‘div’, { className: ‘box’ }, ‘Hello’)` という関数呼び出しに変換されます。これが評価された結果生成されるのが `ReactElement` です。
Reactのコアエンジンは、この `ReactElement` のツリー(仮想DOM)を再帰的に走査し、最終的にブラウザのDOM API(`document.createElement` や `appendChild` など)を叩いて実画面に描画します。
ここで重要なのは、画面に描画される手前の段階では、テキストや数値、配列などもすべて React がいい感じに解釈してよしなに扱ってくれているという点です。
だからこそ、コンポーネントの「子孫として何でも置けるようにしたい」という汎用的な用途では、あらゆる型を網羅する `ReactNode` がデフォルトの選択肢になるわけです。
—
3. 実務での厳密な使い分け基準:いつ、どちらを使うべきか?
では、現場のコードベースではどう使い分けるべきでしょうか。シニアとしての私からの基準は非常にシンプルです。
パターンA: `React.ReactNode` を使うべきケース(基本はこっち)
- レイアウト系コンポーネント(Layout, Modal, Cardなど)
- 子要素として「文字列」や「数値」、あるいは「複数の要素(フラグメントや配列)」が渡ってくることが完全に想定される場合。
パターンB: `React.ReactElement` を使うべきケース(ここが重要)
- 親コンポーネントが、子要素の「Props」や「型(Component Type)」を直接いじったり、制限したりする必要がある場合。
- 例:タブUIやステップフォームなどで、「特定のカスタムコンポーネント(例: `
`)以外の子要素を絶対に許したくない」というアーキテクチャ上の制約をかけたい時。
—
4. 実戦!コピペで使えるコード例
百聞は一見にしかず。現場でよくある「タブ切り替えUI」を例に、`ReactElement` を使って型安全に子要素を制御するイケてる実装を見てみましょう。
このコードでは、親である `Tabs` コンポーネントが、子として渡されるのが本当に `
import React, { ReactElement, useState } from ‘react’;
// ==========================================
// 1. 子コンポーネントの型と定義
// ==========================================
type TabPanelProps = {
label: string;
children: React.ReactNode; // 中身は普通のコンテンツなので ReactNode
};
// 単なる目印(実態は空の関数またはフラグメント)
export const TabPanel: React.FC
return
;
};
// ==========================================
// 2. 親コンポーネントの型と実装
// ==========================================
type TabsProps = {
// ★ ここであえて React.ReactNode ではなく React.ReactElement を強制する
// さらに、配列として複数来ることも考慮してジェネリクスや union にする
children: ReactElement
};
export const Tabs: React.FC
const [activeIndex, setActiveIndex] = useState(0);
// 子要素を配列に正規化(単一の要素が渡された場合への対策)
const childArray = React.Children.toArray(children) as ReactElement
return (
))}
{/ アクティブなタブの中身を表示 /}
);
};
// ==========================================
// 3. 実際の使用例
// ==========================================
export const App = () => {
return (
ここにプロフィール情報が入ります。
ここに設定画面が入ります。
{/
もしここに素の文字列や
TypeScriptのコンパイラが「型が違う!」と怒ってくれます。
/}
);
};
このコードの何がスゴいのか?
もしここで `children` の型を単なる `React.ReactNode` にしていたら、`child.props.label` にアクセスした瞬間に TypeScript は 「Property ‘label’ does not exist on type ‘ReactNode’.」 とコンパイルエラーを吐きます(`ReactNode` には文字列や `null` も含まれるため、`.props` なんて持っている保証がないからです)。
`ReactElement
—
まとめ
最後にシニアからのアドバイスです。
1. 迷ったらまずは `React.ReactNode` を使え。(大半のUIライブラリやコンポーネントの `children` はこれで事足ります)
2. 「親から子をコントロールしたい」「コンポーネントの型を制限したい」という強い意志がある時だけ `React.ReactElement` を選べ。
この使い分けができるようになるだけで、あなたの書くReactコードの品質と堅牢性は一段上のステージに上がります。型エラーに怯える日々を卒業し、型を味方につけた攻めのフロントエンド開発を楽しみましょう!

コメント