こんにちは、かつコーチです。
前回はフォームの作り方と制御コンポーネントについて解説しました。
Reactでアプリを作っていると、コンポーネントはどんどん分割されていきます。
そうなると必ず必要になるのが「親から子へ」「子から親へ」データを受け渡す方法です。
今回はこの基本パターンを整理して解説していきます。
親から子へデータを渡す(props)
propsの基本
親コンポーネントから子コンポーネントへデータを渡すには、propsを使います。
propsとは、コンポーネントに外から渡す「引数」のようなものです。
type GreetingProps = {
name: string;
};
function Greeting({ name }: GreetingProps) {
return <p>こんにちは、{name}さん!</p>;
}
function App() {
return <Greeting name="かつコーチ" />;
}
export default App;
<Greeting name="かつコーチ" />のように、HTMLタグの属性を書く感覚で値を渡せます。
子コンポーネント側は、その値を引数(オブジェクト)として受け取り、そのまま表示や処理に使います。
props経由で複数の値を渡す
propsは1つだけでなく、複数まとめて渡すこともできます。
type UserCardProps = {
name: string;
age: number;
isOnline: boolean;
};
function UserCard({ name, age, isOnline }: UserCardProps) {
return (
<div>
<p>{name}({age}歳)</p>
<p>{isOnline ? "オンライン" : "オフライン"}</p>
</div>
);
}
function App() {
return <UserCard name="かつコーチ" age={30} isOnline={true} />;
}
export default App;
型(UserCardProps)を定義しておくと、渡し忘れや型の不一致をエディタが教えてくれるので、propsが増えてきたら積極的に型を明示しましょう。
子から親へデータを渡す(コールバック関数)
「関数を渡す」という発想
propsは親から子への一方通行なので、逆方向(子から親)には直接データを送れません。
そこで使うのが「関数をpropsとして渡す」というテクニックです。
親が用意した関数を子に渡しておき、子側で何かイベントが起きたときにその関数を呼び出す、という流れになります。
import { useState } from "react";
type ChildButtonProps = {
onNotify: (message: string) => void;
};
function ChildButton({ onNotify }: ChildButtonProps) {
return (
<button onClick={() => onNotify("子コンポーネントから通知しました")}>
通知する
</button>
);
}
function ParentComponent() {
const [message, setMessage] = useState("");
return (
<div>
<ChildButton onNotify={(msg) => setMessage(msg)} />
<p>受け取ったメッセージ: {message}</p>
</div>
);
}
export default ParentComponent;
子コンポーネントはonNotifyという関数をpropsとして受け取っているだけで、その中身がどう実装されているかは知りません。
親側だけが「子から呼ばれたらstateを更新する」という具体的な処理を持っている、という役割分担です。
フォームの値を親に渡す実例
前回解説した制御コンポーネントと組み合わせると、こんな実例になります。
import { useState } from "react";
type SearchBoxProps = {
onSearch: (keyword: string) => void;
};
function SearchBox({ onSearch }: SearchBoxProps) {
const [keyword, setKeyword] = useState("");
const handleSubmit = () => {
onSearch(keyword);
};
return (
<div>
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<button onClick={handleSubmit}>検索</button>
</div>
);
}
function SearchPage() {
const [result, setResult] = useState("");
return (
<div>
<SearchBox onSearch={(keyword) => setResult(`「${keyword}」で検索しました`)} />
<p>{result}</p>
</div>
);
}
export default SearchPage;
SearchBoxが入力値の管理を担当し、SearchPageが検索結果の表示を担当する、というように責任を分割できるのがこのパターンの強みです。
よくあるつまずきポイント・エラー対処
つまずき1:propsをそのまま子コンポーネント内で書き換えようとする
私が実際にやってしまった失敗が、propsで受け取った値を子コンポーネント側で直接書き換えようとしたことです。
❌ Before:propsを直接書き換えようとする
type CounterProps = {
count: number;
};
function CounterDisplay({ count }: CounterProps) {
const increment = () => {
count = count + 1; // propsを直接書き換えている
console.log(count);
};
return (
<div>
<p>{count}</p>
<button onClick={increment}>+1</button>
</div>
);
}
export default CounterDisplay;
このコードはエラーにはならないものの、ボタンを押しても画面の表示は一切変わりません。
countはあくまで親から渡された「読み取り専用の値」であり、直接代入しても再レンダリングは発生しないためです。
私はこの挙動に気づかず、「なぜコンソールの数値は増えているのに画面が変わらないんだろう」としばらく悩みました。
✅ After:更新用の関数をpropsで受け取り、親のstateを更新する
import { useState } from "react";
type CounterDisplayProps = {
count: number;
onIncrement: () => void;
};
function CounterDisplay({ count, onIncrement }: CounterDisplayProps) {
return (
<div>
<p>{count}</p>
<button onClick={onIncrement}>+1</button>
</div>
);
}
function CounterApp() {
const [count, setCount] = useState(0);
return (
<CounterDisplay count={count} onIncrement={() => setCount(count + 1)} />
);
}
export default CounterApp;
「propsは読み取り専用、値を変えたいときは必ず親のstateを更新する関数を呼ぶ」という原則を覚えておくと、この種の混乱を避けられます。
つまずき2:孫コンポーネントまでpropsをバケツリレーしてしまう
コンポーネントの階層が深くなると、propsを何段階も渡し続ける「プロップスドリリング(props drilling)」が発生しがちです。
2〜3階層程度なら問題ありませんが、5階層、6階層と深くなると、中間のコンポーネントが「自分は使わないのに、ただ受け渡すためだけのprops」だらけになってしまいます。
こうなったサインが出たら、後述するContext APIやstate管理ライブラリの導入を検討するタイミングです。
応用・一歩先の使い方
children propsで柔軟な構造を作る
propsの中でも特殊な存在がchildrenです。
タグの中に書いた内容がそのまま子として渡される仕組みで、カードやモーダルのような「中身が可変な入れ物」を作るときに便利です。
import { ReactNode } from "react";
type CardProps = {
children: ReactNode;
};
function Card({ children }: CardProps) {
return <div className="card">{children}</div>;
}
function App() {
return (
<Card>
<h2>タイトル</h2>
<p>本文がここに入ります</p>
</Card>
);
}
export default App;
Cardコンポーネント自身は中身の内容を知らなくても、外側から自由にコンテンツを差し込めます。
深い階層への受け渡しにはContext APIも検討する
先ほどのプロップスドリリング問題への対処法として、ReactにはContext APIという仕組みが用意されています。
これを使うと、間の階層を経由せずに、必要なコンポーネントへ直接値を届けられます。
ただしContext APIは万能ではなく、使いすぎるとコンポーネント同士の関係が見えにくくなるという副作用もあります。
「2〜3階層程度ならpropsのバケツリレーで十分」「それ以上深くなりそうならContext APIを検討する」という判断軸を持っておくとよいでしょう。
まとめ
この記事のポイント
- 親から子へは
propsで値を渡す - 子から親へは、親が用意した関数をpropsとして渡し、子側で呼び出す
- propsは読み取り専用であり、直接書き換えても画面は更新されない
- 階層が深くなりすぎると「プロップスドリリング」が発生し、見通しが悪くなる
childrenpropsを使うと、中身が可変な柔軟なコンポーネントを作れる
次に読むべき記事
次回は、Reactの中心的な機能であるuseStateをさらに詳しく掘り下げ、オブジェクトや配列を安全に更新する方法を解説します。
親子間のデータ受け渡しと組み合わせて理解しておくと、実装の幅がぐっと広がります。
→ 次の記事:useStateをもっと詳しく:オブジェクト・配列の更新