1. ホーム
  2. React

【React】React.Children と cloneElement の使い方|子要素を数える・加工する

Share

親コンポーネントに渡された children を1つずつ数えたり、加工したりしたくなる場面があります。ところが children をそのまま map しようとすると、うまく動かないことがあります。これは children が「必ず配列」とは限らないためです。この記事では、React.Children がなぜ必要になるのか、map / count / toArray / only の役割、そして cloneElement で子要素に props を差し込む書き方を、Tabs のような親コンポーネントの例とあわせて解説します。あわせて、これらを現代の React で乱用しないほうがよい理由にも触れます。

children をそのまま map できない理由

children は「タグで囲んだ中身」を受け取る特別な prop ですが、その中身の型は渡され方によって変わります。子が1つだけなら単一の要素、複数なら配列、何も渡されなければ undefined、文字列だけなら文字列、というように一定しません。そのため children.map(...) と書くと、子が1つしかないケースでは map が存在せず、「children.map is not a function」のような実行時エラーになってしまいます。

この「配列とは限らない」という不確かさを吸収してくれるのが React.Children です。中身が単一要素でも配列でも undefined でも同じように扱え、走査・カウント・配列化といった操作を安全に行えます。まずは提供されているメソッドを整理しておきましょう。

React.Children が提供するメソッド

React.Children には、子要素を扱うためのユーティリティがまとまっています。それぞれの役割は次のとおりです。

メソッド役割
React.Children.map(children, fn)各子要素に関数を適用し、新しい配列を返す。nullundefined の子は自動でスキップされる
React.Children.forEach(children, fn)map と同じように走査するが、戻り値の配列を作らない(副作用が目的のとき用)
React.Children.count(children)子要素の個数を返す。単一要素でも配列でも正しく数えられる
React.Children.toArray(children)children を平坦なフラット配列に変換し、各要素に安定した key を振る
React.Children.only(children)子要素がちょうど1つであることを保証し、その要素を返す。1つでなければ例外を投げる

いずれも第1引数に children をそのまま渡せる点が共通しています。以降では、実際に使う機会の多い mapcount、そして toArray を中心に見ていきます。

React.Children.map と count で子要素を走査する

各子要素に番号を振って表示する例です。React.Children.map は子が1つでも複数でも同じように動き、コールバックの第2引数でインデックスを受け取れます。React.Children.count で全体の個数も取得しています。

NumberedList.tsx
import { Children, type ReactNode } from 'react';

type NumberedListProps = {
  children: ReactNode;
};

function NumberedList({ children }: NumberedListProps) {
  // children は配列とは限らないが、Children.map なら安全に走査できる
  const total = Children.count(children);

  return (
    <div>
      <p>全{total}件</p>
      <ul>
        {Children.map(children, (child, index) => (
          <li>{index + 1}. {child}</li>
        ))}
      </ul>
    </div>
  );
}

// 子が1つでも複数でも、そのまま動く
function App() {
  return (
    <NumberedList>
      <span>りんご</span>
      <span>みかん</span>
    </NumberedList>
  );
}

もしここで children.map(...) と直接書いていたら、子が <span>りんご</span> の1つだけになった瞬間にエラーになります。React.Children.map は単一要素を内部で配列のように扱ってくれるため、渡し方を気にせず走査できるのが利点です。

cloneElement で各子要素に props を差し込む

cloneElement は、既存の要素を複製しつつ props を追加・上書きした新しい要素を作る関数です。これを React.Children.map と組み合わせると、親が受け取った各子要素に、状態やインデックスなどの共通の props をまとめて注入できます。タブ切り替えの Tabs や、選択肢をまとめる RadioGroup のような親コンポーネントでよく使われるパターンです。

次の例では、親の Tabs が現在アクティブなインデックスを状態として持ち、各 TabisActiveonSelect を注入しています。子側は props を受け取るだけで、状態管理は親に任せられます。

Tabs.tsx
import {
  Children,
  cloneElement,
  isValidElement,
  useState,
  type ReactNode,
  type ReactElement,
} from 'react';

type TabProps = {
  label: string;
  isActive?: boolean;
  onSelect?: () => void;
};

// 子要素として並べる Tab。状態は持たず、props を受け取るだけ
function Tab({ label, isActive, onSelect }: TabProps) {
  return (
    <button
      onClick={onSelect}
      style={{ fontWeight: isActive ? 'bold' : 'normal' }}
    >
      {label}
    </button>
  );
}

function Tabs({ children }: { children: ReactNode }) {
  const [active, setActive] = useState(0);

  return (
    <div>
      {Children.map(children, (child, index) => {
        // 要素でないもの(文字列など)はそのまま返す
        if (!isValidElement(child)) return child;

        // 各 Tab に現在の状態とクリック時の動作を注入する
        return cloneElement(child as ReactElement<TabProps>, {
          isActive: index === active,
          onSelect: () => setActive(index),
        });
      })}
    </div>
  );
}

function App() {
  return (
    <Tabs>
      {/* 子側では isActive も onSelect も書かなくてよい */}
      <Tab label="ホーム" />
      <Tab label="設定" />
    </Tabs>
  );
}

ポイントは isValidElement で「本当に React 要素か」を確認してから cloneElement を呼んでいるところです。children の中には文字列や null が混じることがあり、それらを複製しようとすると壊れてしまうためです。cloneElement の第2引数に渡した props は元の要素の props にマージされ、同じキーは新しい値で上書きされます。これにより、呼び出し側は <Tab label="ホーム" /> と書くだけで、状態に応じた表示が親から流し込まれるわけです。

toArray で key を安定させる

子要素を並べ替えたり、一部だけを抜き出して表示したりしたいときは、React.Children.toArray が便利です。これは children をふつうの配列に変換すると同時に、各要素に安定した key を自動で振ってくれます。手動で key を付け直さなくても、並べ替えの前後で React が同じ要素だと認識できるため、無駄な再マウントを防げます。

Reversed.tsx
import { Children, type ReactNode } from 'react';

function Reversed({ children }: { children: ReactNode }) {
  // toArray は null/undefined を除いた平坦な配列を返し、
  // 各要素に安定した key を振ってくれる
  const items = Children.toArray(children);

  // key が安定しているので、逆順にしても状態が保たれる
  return <div>{items.reverse()}</div>;
}

もし React.Children.map の戻り値をそのまま逆順にすると、位置ベースの key が振り直されてしまい、並べ替えたつもりが「別の要素に置き換わった」と React に判断されることがあります。toArray は元の要素の key を尊重した安定したキーを生成するので、順序を入れ替える処理と相性が良いのです。加えて、nullfalse といった描画されない子を取り除いてくれるため、「実際に表示される子だけ」を扱いたいときにも役立ちます。

React.Children や cloneElement に頼りすぎない

ここまで便利な機能を紹介してきましたが、現代の React ではこれらを第一の手段にしないほうがよいとされています。多くのケースは、より素直な書き方で置き換えられるからです。

状態や設定は context や props で渡す

Tabs の例のように親から子へ状態を配りたい場合、cloneElement で props を差し込む代わりに、Context を使って子側から useContext で受け取る設計にできます。こうすると、間に別の要素が挟まっても値が届き、どの子がどんな値を受け取るかもコードから追いやすくなります。項目のリストを扱いたいだけなら、children を使わずにデータの配列を props で受け取り、親の中で map して描画するほうがシンプルなことも多いです。

cloneElement は子の実装に暗黙的に依存する

cloneElement の弱点は、親が子に何を注入しているかが呼び出し側から見えないことです。<Tab label="ホーム" /> と書いた開発者は、裏で isActiveonSelect が差し込まれていることに気づきにくく、子の props 名を変えると親が黙って壊れることもあります。深いネストの子要素まで再帰的に加工するような使い方は、とくに壊れやすく読みにくくなるため避けたほうが無難です。子要素を数えたい・並べ替えたいといった軽い用途にとどめ、props の受け渡しは明示的な設計を優先しましょう。

React 19 では legacy 扱いの方向にある

公式ドキュメント(react.dev)では、ChildrencloneElement はどちらも「Legacy React APIs」として分類され、代替手段の利用が推奨される方向になっています。すぐに削除されるわけではなく、既存のコードが動かなくなるという話でもありませんが、React 側としては非推奨に寄せていく流れだと理解しておくとよいでしょう。新しくコンポーネントを設計するときは、まず context や明示的な props で解決できないかを検討し、それでも子要素そのものを走査・加工する必要がある場合に限ってこれらの API を使う、という順番がおすすめです。

まとめ

children は単一要素・配列・undefined など渡され方で型が変わるため、そのまま map できません。React.Children はこの違いを吸収し、map で走査、count で個数取得、toArray で安定した key 付きの配列化、only で子が1つであることの保証、といった操作を安全に行えます。各子要素へ共通の props を配りたいときは cloneElementReact.Children.map と組み合わせ、isValidElement で要素かどうかを確認してから複製するのが定石です。ただし、これらは公式ドキュメントで legacy に分類されつつある API でもあります。多くの場面は context や明示的な props で置き換えられるので、子要素を数える・並べ替えるといった用途に絞り、深い加工は避けるのが、現代の React での付き合い方です。

参考ページ