React.Children で子要素を走査して、入力 / イベントを差し込む方法
Reactで「表示用のコンポーネントはそのままに、外側(Wrapper)から入力やイベントを差し込みたい」ことがあります。
たとえば、
- 子側は「見せたい内容」だけ書きたい
- 外側で
valueやonChangeをまとめて注入したい - 深い入れ子でも、必要な要素だけに注入したい
この記事では React.Children を使って子要素を走査し、特定のコンポーネントにだけ props を差し込む方法を、最小例で解説します。
注意:この記事の手法は便利ですが、依存が増えやすい面もあります。
やりたいこと(完成イメージ)
子側は「こう表示したい」だけを書きます。
<Injector value={name} onChange={setName} onSubmit={() => console.log("submit")}>
<div>
<h3>プロフィール</h3>
<TargetInput label="名前" />
<section>
<TargetInput label="ニックネーム" />
</section>
</div>
</Injector>
すると TargetInput にだけ、外側から value / onChange / onSubmit が注入されます。
仕組みの要点
-
React.Children.map(children, ...) で子要素を走査する
-
React.isValidElement で「React要素か?」を判定する
-
注入対象(例:TargetInput)だけを識別する
-
React.cloneElement で props を差し込む
-
ネストした子も対象にするなら、同じ処理を再帰で回す
注入される側:TargetInput(目印になるコンポーネント)
TargetInput は「注入される前提」のコンポーネントです。
(InjectedProps は Partial にしておくと、単体でも表示できます)
import React from "react";
export type InjectedProps = {
value: string;
onChange: (next: string) => void;
onSubmit: () => void;
};
export function TargetInput(
props: { label: string } & Partial<InjectedProps>
) {
return (
<div style={{ marginTop: 8 }}>
<div style={{ fontSize: 12, opacity: 0.8 }}>{props.label}</div>
<input
placeholder="(injected)"
value={props.value ?? ""}
onChange={(e) => props.onChange?.(e.target.value)}
style={{ padding: 6 }}
/>
<button
type="button"
onClick={() => props.onSubmit?.()}
style={{ marginLeft: 8 }}
>
submit
</button>
</div>
);
}
差し込む側:Injector(React.Children で走査して注入する)
この Injector が本題です。
-
TargetInput だけ注入する(それ以外には触らない)
-
ネストされた children も探索して注入する(再帰)
import React from "react";
import { TargetInput, InjectedProps } from "./TargetInput";
type InjectorProps = {
children: React.ReactNode;
value: string;
onChange: (next: string) => void;
onSubmit: () => void;
};
export function Injector({ children, value, onChange, onSubmit }: InjectorProps) {
const inject = (node: React.ReactNode): React.ReactNode => {
return React.Children.map(node, (child) => {
// 文字列やnullなどはそのまま返す
if (!React.isValidElement(child)) return child;
// 注入対象の識別(TargetInputだけ)
if (child.type === TargetInput) {
return React.cloneElement(child, {
value,
onChange,
onSubmit,
} satisfies InjectedProps);
}
// ネストされたchildrenがあれば再帰して差し替える
const nested = (child.props as { children?: React.ReactNode }).children;
if (nested) {
return React.cloneElement(child, {
children: inject(nested),
});
}
// それ以外は触らない
return child;
});
};
return <>{inject(children)}</>;
}
使い方
import React, { useState } from "react";
import { Injector } from "./Injector";
import { TargetInput } from "./TargetInput";
export function Demo() {
const [name, setName] = useState("");
return (
<Injector
value={name}
onChange={setName}
onSubmit={() => alert(`submit: ${name}`)}
>
<div>
<h3>プロフィール</h3>
{/* ここだけ注入される */}
<TargetInput label="名前" />
{/* ネストしても注入される */}
<section>
<TargetInput label="ニックネーム" />
</section>
{/* これは注入されない */}
<p style={{ marginTop: 8 }}>注入対象は TargetInput のみ</p>
</div>
</Injector>
);
}
どうやって「注入対象」を見分ける?
今回の例はこれです:
- child.type === TargetInput
これは「同じ参照のコンポーネント」に限定されます。
別ファイル経由やラップ構造が増えると判定が崩れることもあるので、状況次第で以下も候補になります。
-
displayName を付けて判定する(開発時に分かりやすい)
-
props にフラグを渡して判定する(最も明示的)
例:AnyComponent data-inject-target -
そもそも注入ではなく Render Props / Context に切り替える
この方法が向いている場面 / 向いていない場面
向いている
-
「表示部分(子)」を差し替えできるようにしたい
-
何種類もあるUIに、共通のイベントをまとめて差し込みたい
-
深い入れ子でも、特定の要素だけを狙って注入したい
向いていない(つらくなりやすい)
-
注入対象が増えすぎて「どこで何が入るか」追いづらい
-
注入される側が多機能になって責務が膨らむ
-
cloneElement に頼りすぎて壊れやすくなる
まとめ
-
React.Children.map と cloneElement を使うと、子要素を走査して props を差し込める
-
注入対象は必ず「明確に」見分ける(全部に注入しない)
-
ネスト探索が必要なら、子の children を再帰で差し替える
以上、読んでいただきありがとうございました〜!