【React】親子コンポーネント間でのデータの受け渡し方

JavaScript

こんにちは、かつコーチです。

前回はフォームの作り方と制御コンポーネントについて解説しました。

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をもっと詳しく:オブジェクト・配列の更新

タイトルとURLをコピーしました