Reactのchildrenとpropsの違い|型定義・設計パターン・React 19の変更点
Reactのchildrenは、コンポーネントタグの中にJSXをネストしたときに渡ってくるpropsです。propsの一種でありながら、渡し方も型定義も他のpropsとは違います。しかも周辺仕様はReact 19で動きました。関数コンポーネントの defaultProps は削除され、ref は通常のpropsになり、TypeScriptのグローバル JSX 名前空間は無くなっています。加えて公式ドキュメントは React.Children と cloneElement に「壊れやすいコードにつながる」という警告を掲げています。この記事では最新安定版のReact 19.2.8(2026年7月21日リリース)と @types/react 19.2.17 の実装を確認しながら、childrenの受け取り方・型付け・設計パターン・移行が必要な箇所を整理します。
まとめ
先に結論を並べます。
- propsは「値」を渡す口、childrenは「構造」を渡す口です。呼び出し側がJSXの中身を決めるならchildrenにします。
- TypeScriptでは
React.FCにchildrenが含まれません。PropsWithChildrenを使うかchildren: ReactNodeを自分で書きます。前者はchildrenが任意扱いになります。 React.ChildrenとcloneElementには公式が非推奨寄りの警告を出しています。新規実装では代替(複数コンポーネント公開/配列props/render prop/Context)を選びます。- 状態を持つコンポーネントにchildrenを外から渡すと、その状態更新でchildren側は再レンダリングされません。React Compiler 1.0を入れてもこの構造の価値は消えません。
- React 19では
defaultProps削除・refのprops化・型定義の破壊的変更があり、childrenを受け取るラッパーほど書き換えが必要です。
以下、判断基準とコードを順に見ていきます。
propsとchildrenの役割分担
JSXで挟んだ中身が入る予約済みpropsとしてのchildren
コンポーネントタグの中にJSXをネストすると、その中身が children という名前のpropsとして渡ります。属性で渡す他のpropsと入り口が違うだけで、受け取る側から見れば props オブジェクトの1プロパティにすぎません。
function Card({ title, children }) {
return (
<section className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
</section>
);
}
// 呼び出し側
<Card title="通知設定">
<p>メール通知を有効にしました。</p>
<button>設定を開く</button>
</Card>
// props.title = "通知設定"(値)
// props.children = <p> と <button> の2要素(構造)
title は文字列を1つ受け取るだけですが、childrenは要素が何個来るか、そもそも来るかどうかも呼び出し側次第です。この非対称性が、後述する型定義とReact.Children APIの落とし穴の出どころになります。
propsとchildrenに共通する不変(immutable)の制約
React公式ドキュメントはpropsを「不変(immutable)」と表現し、変更が必要なときは親が別のpropsを渡し直すよう求めています。childrenも同じ制約下にあり、受け取った側が中身の要素にプロパティを足すことはできません。
この制約こそが、子要素にpropsを注入する目的で cloneElement が使われてきた理由であり、同時にそれが「データフローを追いにくくする」と公式に指摘される理由でもあります。その要素にどこで何が足されたのかが、JSXの見た目から消えるためです。
props/childrenを選ぶ5つの判断軸
| 判断軸 | props | children |
|---|---|---|
| 渡すもの | 値・設定・コールバック | JSXの構造 |
| 表示の決定権 | 受け取る側 | 呼び出す側 |
| 個数 | 1プロパティ1値 | 0個〜n個・不定 |
| 型の広さ | 指定した型のみ | ReactNode(文字列・配列・nullも可) |
| Server Component境界 | インライン関数は不可 | 要素は可 |
迷ったときは「表示の形をどちらが決めるか」で切ると外れにくくなります。ボタンのラベル文字列のように受け取る側が体裁を決めるならprops、モーダルの中身のように呼び出す側が自由にレイアウトするならchildrenです。表の最終行はServer Componentsを使う場合の制約です。
TypeScriptでのchildrenの型定義
React.FCの型定義にchildrenが無い理由と回避策
@types/react 19.2.17 の FunctionComponent は次の形です。型引数 P の既定値は {} で、childrenはどこにも現れません。
// @types/react 19.2.17 の実体(抜粋)
interface FunctionComponent<P = {}> {
(props: P): ReactNode | Promise<ReactNode>;
displayName?: string | undefined;
}
そのため const Card: React.FC<{ title: string }> = ({ title, children }) => ... はコンパイルエラーになります。@types/react 18 で暗黙のchildrenが外されて以降の仕様で、React 19系でも変わっていません。戻り値に Promise<ReactNode> が入っているのは、非同期のServer Componentを React.FC で型付けできるようにするためです。
PropsWithChildrenでchildrenが任意になる落とし穴
公式が用意しているヘルパ型の定義はこうなっています。children に ? が付いている点が重要です。
// @types/react 19.2.17 の実体
type PropsWithChildren<P = unknown> = P & { children?: ReactNode | undefined };
つまり PropsWithChildren で型付けしたコンポーネントは、childrenを何も渡さない <Card title="x" /> が型エラーになりません。中身が空だと成立しないコンポーネント(モーダル、レイアウトのスロットなど)では、ヘルパを使わず自分で書いたほうが安全に倒せます。
// A: 省略を許す場合(PropsWithChildren と同義)
type CardProps = React.PropsWithChildren<{ title: string }>;
// B: 中身を必須にしたい場合は自分で書く
type ModalProps = {
title: string;
children: React.ReactNode; // ? を付けない
};
function Modal({ title, children }: ModalProps) {
return <div role="dialog" aria-label={title}>{children}</div>;
}
ReactNodeとReactElementの使い分け
ReactNode の定義も型定義ファイルで確認できます。
// @types/react 19.2.17 の実体(抜粋。実際には Promise<AwaitedReactNode> 等が続く)
type ReactNode =
| ReactElement
| string
| number
| bigint
| Iterable<ReactNode>
| ReactPortal
| boolean
| null
| undefined;
文字列も数値も配列も null も通る、最も広い型です。childrenの型としては基本これで足ります。一方「必ず単一のReact要素であること」を型で保証したい場合は ReactElement を使います。タブやアコーディオンのように、子の型を限定したいコンポーネントで効きます。
// 子は必ず単一の要素であることを要求する
type TabPanelProps = { children: React.ReactElement };
// 関数を子として受け取る(Function as Children)
type FetcherProps<T> = { children: (data: T) => React.ReactNode };
ただし ReactElement で縛っても、呼び出し側がFragmentで包めば「単一の要素」の条件は形式上満たされます。型は入り口の形を制約するだけで、中身の構造までは保証しません。
React.ChildrenとcloneElementを公式が勧めない理由
react.devに掲示されている2つの警告文
react.dev の該当ページには、どちらにも同じ趣旨のPitfallが置かれています。Childrenのページは「Using Children is uncommon and can lead to fragile code.(Childrenを使うのは一般的ではなく、壊れやすいコードにつながりうる)」。cloneElementのページは「Using cloneElement is uncommon and can lead to fragile code.」に加えて「cloneElement makes it harder to trace the data flow(データフローを追いにくくする)」と明記しています。
いずれも削除されたAPIではなく、React 19.2.8でも動きます。ただし公式が推奨するのは代替側です。React.Children.map と cloneElement の組み合わせを主要な手法として紹介する日本語記事は今も多いのですが、そこは現在の公式方針から外れています。
Fragmentを走査しない仕様が招く3つの破損パターン
React.Children には count、forEach、map、only、toArray の5メソッドがあります。問題は走査の深さで、公式のCaveatsは「The traversal does not go deeper than React elements(走査はReact要素より深くは進まない)」「Fragments don’t get traversed(Fragmentは走査されない)」と明記しています。
// 子を1つずつ処理する意図で書いたコンポーネント
function Toolbar({ children }) {
return React.Children.map(children, (child, i) =>
React.cloneElement(child, { index: i })
);
}
// 呼び出し側A: 意図どおり index 0,1 が配られる
<Toolbar><Save /><Cancel /></Toolbar>
// 呼び出し側B: Fragment で包んだ瞬間に子は「1個」になる
<Toolbar>
<>
<Save />
<Cancel />
</>
</Toolbar>
// Children.count は 1、index は Fragment に付いて中身には届かない
// 呼び出し側C: 条件分岐で false が混ざる
<Toolbar>
<Save />
{canCancel && <Cancel />}
</Toolbar>
// false は null に正規化されて渡り、cloneElement(null) で実行時エラー
呼び出し側の書き方はコンポーネント側から制約できません。Fragmentを1つ足す、条件分岐を1つ足す、共通ラッパーを1枚挟む。そのどれもが日常的な変更で、そのたびに壊れる実装は設計として持たないほうがよいと考えています。
Children.toArray は空ノード(null・undefined・真偽値)を除去するのでCのケースは救えます。並び替えについても公式は「This ensures that flattening the array does not introduce changes in behavior」としており、sort や reverse を掛ける用法自体は想定内です。注意すべきなのはキーで、元のキーには接頭辞が付き 'a' が '.$a' になります。呼び出し側が明示キーを付けていない場合は位置由来のキー('.0'、'.1')になるため、並び替えでコンポーネントのstateが引き継がれません。child.key で元データと突合する実装も崩れます。
目的別に見るReact.Children/cloneElementの代替
公式が挙げる代替を、目的別に対応付けると次のようになります。
| やりたいこと | 選ぶ手段 | 理由 |
|---|---|---|
| 子を一律に同じ枠で包む | ラッパー用コンポーネントを公開 | 責務が呼び出し側JSXに出る |
| 子ごとに構造化データが要る | 配列をpropsで渡す | キーとデータを型で保証 |
| 描画方法を呼び出し側に委ねる | render prop | 受け渡しが引数で見える |
| 深い階層へ値を配る | Context | 中間層を経由しない |
| 非表示ロジックを共有する | カスタムHook | 描画と切り離せる |
先ほどのToolbarを配列propsで書き直すと、Fragmentも条件分岐も壊しようがなくなります。
type ToolbarProps = {
items: { id: string; content: React.ReactNode }[];
};
function Toolbar({ items }: ToolbarProps) {
return (
<div className="toolbar">
{items.map((item, index) => (
<div key={item.id} data-index={index}>{item.content}</div>
))}
</div>
);
}
cloneElement を残してよいのは、外部ライブラリのAPIがそれを前提に設計されている場合だけです。自社コードの新規実装で選ぶ理由はありません。
childrenを軸にした設計パターン
Compound Component:スロット分割による構造の明示
子の中身を走査する代わりに、置き場所ごとにコンポーネントを分けて公開します。内部の共有状態はContextで配ります。
const CardContext = React.createContext<{ collapsed: boolean } | null>(null);
function Card({ collapsed = false, children }: React.PropsWithChildren<{ collapsed?: boolean }>) {
return (
<CardContext.Provider value={{ collapsed }}>
<section className="card">{children}</section>
</CardContext.Provider>
);
}
Card.Header = function CardHeader({ children }: React.PropsWithChildren) {
return <header className="card-header">{children}</header>;
};
Card.Body = function CardBody({ children }: React.PropsWithChildren) {
const ctx = React.useContext(CardContext);
if (ctx?.collapsed) return null;
return <div className="card-body">{children}</div>;
};
// 呼び出し側は構造を明示的に書く
<Card collapsed={false}>
<Card.Header>通知設定</Card.Header>
<Card.Body><p>メール通知を有効にしました。</p></Card.Body>
</Card>
Fragmentで包まれても条件分岐が入っても、各スロットが自分の責務だけを見るので壊れません。Headless UIとは?React/Vueでの使い方・Tailwind連携・主要ライブラリ比較で扱うようなヘッドレスUIライブラリは、ほぼこの構造を採っています。
Function as Children:描画権の呼び出し側への移譲
childrenに関数を渡す形式です。データ取得や状態管理だけを担い、描画は呼び出し側に委ねたいときに使います。
type ListProps<T> = {
items: T[];
children: (item: T, index: number) => React.ReactNode;
};
function List<T>({ items, children }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{children(item, i)}</li>)}</ul>;
}
<List items={users}>
{(user) => <span>{user.name}({user.role})</span>}
</List>
フォームライブラリのAPIにもこの形は多く見られます。React Hook Formとは?使い方・バリデーション・v7の書き方を実例で解説で触れる Controller の render も同じ発想で、値と操作関数を引数として明示的に渡しています。
Server Components境界での関数childrenの制約
ここは判断を分ける制約なので明記しておきます。React Server Componentsの境界を越えて渡せるのはシリアライズ可能な値と要素で、関数は 'use server' を付けたServer Functionsだけが例外です。render propのようなインライン関数は渡せないため、Function as Childrenを採るコンポーネントは呼び出し側もろともClient Componentにする必要が出ます。
// Server Component から Client Component へ
// OK: 要素は境界を越えられる
<ClientPanel><ServerRenderedTable /></ClientPanel>
// NG: インライン関数は越えられない("use client" が呼び出し側にも必要になる)
<ClientList items={rows}>{(row) => <Cell value={row.value} />}</ClientList>
Next.jsのApp Routerのようにサーバ側描画を前提とする構成では、Compound Componentを優先し、Function as Childrenは状態を扱う末端に閉じ込めたほうが境界が広がりません。純粋なクライアントアプリならこの制約は効きません。サーバ側描画そのものの前提はフロントエンド開発者が理解すべきAsync Reactの技術的背景と全体像にまとめています。
childrenを外から渡す再レンダリング最適化
要素参照の同一性によるサブツリー再訪のスキップ
状態を持つコンポーネントの中に重い子を直接書くと、状態更新のたびに子も再レンダリングされます。子を上位で作ってchildrenとして渡すと、この再レンダリングが止まります。Dan Abramov氏は理由をこう説明しています。「it still has the same children prop it got from the App last time, so React doesn’t visit that subtree(前回Appから受け取ったのと同じchildrenのままなので、Reactはそのサブツリーを訪れない)」。
// Before: color が変わるたび ExpensiveTree も再レンダリング
function App() {
const [color, setColor] = React.useState("red");
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<ExpensiveTree />
</div>
);
}
// After: children として外から渡す
function App() {
return (
<ColorPicker>
<ExpensiveTree />
</ColorPicker>
);
}
function ColorPicker({ children }) {
const [color, setColor] = React.useState("red");
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
{children}
</div>
);
}
(コード例はoverreacted.io「Before You memo()」の構成を簡略化したものです。)React.memo も依存配列も使っていません。ColorPicker が再レンダリングされても children の要素オブジェクトは前回と同一の参照なので、Reactはそのサブツリーの差分計算をスキップします。状態の置き場所を1階層動かすだけで得られる効果です。
React Compiler 1.0導入後も残る構造見直しの必要性
babel-plugin-react-compiler は1.0.0が2025年10月7日にnpmへ公開され、実験版の段階を抜けました。再レンダリングのたびに再生成される値やコンポーネントを自動でメモ化するため、手書きの useMemo や React.memo の多くは不要になります。
ただしコンパイラがやるのはメモ化であって、状態の置き場所を動かすことではありません。childrenの持ち上げはコンポーネントの構造そのものを変える手当てなので、性質が違います。導入判断の材料はReact Compilerとは?自動メモ化の仕組みと導入方法・useMemoとの違いを解説にまとめています。
React 19で変わったchildren周辺の仕様
defaultProps削除とES6デフォルト引数への移行
React 19は関数コンポーネントの defaultProps を削除しました。クラスコンポーネントはES6に代替手段が無いため残されています。propTypes も同時に削除され、指定しても黙って無視されます。
// React 18 まで(React 19 では無視される)
function Card({ title, children }) { /* ... */ }
Card.defaultProps = { title: "無題" };
// React 19 以降
type CardProps = React.PropsWithChildren<{ title?: string }>;
function Card({ title = "無題", children }: CardProps) { /* ... */ }
// children 自体にデフォルトを置くこともできる
function Card({ title = "無題", children = <p>内容がありません</p> }: CardProps) {
return <section><h3>{title}</h3>{children}</section>;
}
propTypes からTypeScriptへの移行には公式のcodemod npx codemod@latest react/prop-types-typescript が用意されています。
ref as propによるforwardRefラッパーの不要化
React 19では関数コンポーネントが ref を通常のpropsとして受け取れます。リリースノートは「New function components will no longer need forwardRef」とし、さらに「In future versions we will deprecate and remove forwardRef(将来のバージョンでforwardRefを非推奨化し削除する)」と明言しています。childrenを受け取るラッパー系コンポーネントは内側のDOMへrefを転送する用途が多く、影響を受けやすい部分です。
// React 18 まで
const Field = React.forwardRef<HTMLInputElement, { label: string }>(
({ label }, ref) => <label>{label}<input ref={ref} /></label>
);
// React 19 以降
function Field({ label, ref }: { label: string; ref?: React.Ref<HTMLInputElement> }) {
return <label>{label}<input ref={ref} /></label>;
}
あわせて useRef は引数が必須になり、useRef() は型エラーになります。MutableRefObject は非推奨化され、RefObject への一本化が進んでいます(型自体は @types/react 19.2.17 にまだ存在し、JSDocに @deprecated が付いた状態です)。ref自体の扱いはReactのuseRefの使い方|useStateとの違い・DOM参照・値の保持を実例で解説で整理しています。
型定義の破壊的変更:JSX名前空間とReactElementのprops
グローバルの JSX 名前空間が廃止され React.JSX に移りました。複数のUIライブラリがJSX型を拡張したときの衝突を避けるための変更です。モジュール拡張は declare module "react" で包む必要があり、codemodは scoped-jsx です。
// global.d.ts
declare module "react" {
namespace JSX {
interface IntrinsicElements {
"my-element": { myElementProps: string };
}
}
}
tsconfig.json の jsx 設定によって書くモジュール名が変わります。"react-jsx" なら react/jsx-runtime、"react-jsxdev" なら react/jsx-dev-runtime、"react" と "preserve" なら react を指定します。
もう1点、ReactElement["props"] の既定型が any から unknown に変わりました。型引数を明示していれば影響しませんが、childrenの要素からpropsを読むコードはここで型エラーになります。cloneElement や Children.map で子のpropsを覗く実装ほどこの変更に当たりやすいため、移行のタイミングで代替へ寄せるほうが手戻りが少なくて済みます。
type Example = React.ReactElement["props"];
// React 18: any / React 19: unknown
type Example2 = React.ReactElement<{ id: string }>["props"];
// 型引数を渡していれば { id: string } のまま
よくある質問
propsとchildrenはどちらで渡すべきですか
表示の形を呼び出し側が決めるならchildren、受け取る側が決めるならpropsにします。ボタンのラベル文字列や variant="primary" のようなバリアント指定は受け取る側が体裁を決めるのでprops、モーダルやカードの本体は呼び出し側がレイアウトを組むのでchildrenが自然な切り分けです。迷う場合は、その値をJSXとして書きたくなるかどうかで決めてください。値を1つ渡すだけならprops、要素を並べたくなるならchildrenです。
React.FCでchildrenを使うとエラーになるのはなぜですか
@types/react 18以降、FunctionComponent の型引数にchildrenが暗黙で含まれなくなったためです。19.2.17でも interface FunctionComponent<P = {}> のままで、childrenは定義されていません。回避策は2つあり、React.PropsWithChildren で包むか、props型に children: React.ReactNode を自分で追加します。中身を必須にしたい場合は後者を選んでください。前者はchildrenが任意扱いになるためです。
React.Children.mapと配列のmapは何が違いますか
childrenは常に配列とは限らず、子が1つなら要素そのもの、0個なら undefined が入ります。そのため children.map() は実行時エラーになりえます。React.Children.map はこの差を吸収し、空ノードも扱えます。ただしFragmentの内側までは走査しないため、子の構造を前提にした処理は依然として壊れます。構造を扱いたい場合は配列propsやCompound Componentへ寄せるほうが確実です。
childrenの型はReactNodeとReactElementのどちらにすべきですか
特に制約が無ければ ReactNode で足ります。文字列・数値・配列・null まで受け付ける最も広い型で、実際のchildrenの取りうる値と一致するためです。単一の要素であることを要求したい場合のみ ReactElement を使います。React.JSX.Element は ReactElement<any, any> 相当で、React 19からはグローバルの JSX.Element ではなく React.JSX.Element と書く必要があります。
cloneElementはReact 19で使えなくなりましたか
削除されていません。React 19.2.8でも動作します。ただし公式ドキュメントは「壊れやすいコードにつながる」「データフローを追いにくくする」と警告し、render prop・Context・カスタムHookへの置き換えを推奨しています。加えて ReactElement["props"] が unknown になったことで、子のpropsを読む実装はTypeScript側からも書きにくくなりました。新規実装では選ばないほうが安全です。