🪣

【React】Props のバケツリレー解消法について

に公開

はじめに

Reactでは、コンポーネント間のデータの受け渡しにおいて、propsのバケツリレーの問題がよく発生することがあります。
バケツリレーとは、あるコンポーネントから別のコンポーネントに props を渡す際に、複数の中間コンポーネントを経由して渡しているだけの状態をいいます。
なぜこのような問題が発生するかというと、React では「単方向データフロー」を採用していて、コンポーネント間で直接データをやり取りすることができないためだからです。

Context API や状態管理ライブラリで解消する方法もありますが、props を渡すだけの分かりやすいコンポーネントを作成することも重要になってきます。
そこで今回は、不要なコンポーネント層を削除する方法や、コンポジションパターンを用いた解決策について紹介していきたいと思います。

バケツリレーとその問題点

バケツリレーとは?

React では、基本的に親コンポーネントから子コンポーネントへ props を渡します。
しかし、必要なデータが深い階層にある場合、途中のコンポーネントがただ props を中継するためだけに存在しているなんてこともあります。
この状態は以下のような問題を引き起こします。

問題点

  • コードが煩雑になる
    中間のコンポーネントなどの余計なコードが増えてしまい、可読性が悪くなってしまう。
  • パフォーマンスの低下
    不必要なコンポーネントの再レンダリングが発生する可能性があるので、大規模アプリケーションになるほど影響は大きくなる
  • 保守性の低下
    データの受け渡しが複雑になると、どこでデータが変化したのか追跡しづらくなる。

不要なコンポーネント層の削除

まずは、propsのバケツリレーを解消する方法として、不要なコンポーネント層を削除する方法を紹介します。
この方法は、props を渡しているだけのコンポーネントを削除することで、コードをシンプルにし、可読性を向上させることができます。

❌ Bad

この例では、Dashboard コンポーネントから UserSection 経由で Profile にデータが渡され、UserSection が行なっていることは、単に props を渡しているだけです。
このことから、UserSection コンポーネントは不要なコンポーネント層となっていることが分かります。

const App = () => {
  return <Dashboard />;
};

const Dashboard = () => {
  const user = { name: "hodii", age: 12};
  return <UserSection user={user} />;
};

const UserSection = ({ user }) => {
  return <Profile user={user} />;
};

const Profile = ({ user }) => {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.age}</p>
    </div>
  );
};

✅ Good

UserSection コンポーネントを削除することで、Dashboard コンポーネントから Profile コンポーネントに直接 props を渡すようにしました。
これにより、コードがシンプルになり、可読性を向上させることができました。

const App = () => {
  return <Dashboard />;
};

const Dashboard = () => {
  const user = { name: "hodii", age: 12 };
  return <Profile user={user} />;
};

const Profile = ({ user }) => {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.age}</p>
    </div>
  );
};

コンポジションパターンの活用

コンポジションパターンとは、ReactNode を props として受け取る方法です。
コンポジションパターンの代表的な例は children です。
Reactでコンポジションパターンを活用できると、柔軟で再利用性の高いコンポーネントを作成することができます。

Card コンポーネントの例

❌ Bad

以下の例では、Card コンポーネントが title、description、footer という固定の props を受け取り、内部でレイアウトを決めてしまっています。
このような設計では、Card コンポーネントは特定のレイアウトに依存してしまい、他のコンポーネントで再利用する際に柔軟性が失われてしまいます。
例えば、後からボタンやリンクも追加したいとなった場合、Card コンポーネントの props を増やす必要があり、結果としてコンポーネントの責務がどんどん増えていってしまうことにも繋がります。

const Card = ({ title, description, footer }) => {
  return (
    <div>
      <div>
        <h2>{title}</h2>
      </div>
      <div>
        <p>{description}</p>
      </div>
      <div>
        <small>{footer}</small>
      </div>
    </div>
  );
};

const App = () => {
  return (
    <Card
      title="ユーザー情報"
      description="このカードはユーザーの詳細情報を表示します。"
      footer="2025/03/15"
    />
  );
};

✅ Good

Card コンポーネントで children を受け取り、内部でレイアウトを決めることなく、親コンポーネントが自由に内容を定義できるように変更しました。
このようにすることで、Card コンポーネントは特定のレイアウトに依存せずに、他のコンポーネントで再利用する際にも柔軟性を保つことができます。
このようにしておけば、後から要件が変更されても柔軟に対応可能にすることができます。

const Card = ({ children }) => {
  return <div>{children}</div>;
};

const App = () => {
  return (
    <Card>
      <div>
        <h2>ユーザー情報</h2>
      </div>
      <div>
        <p>このカードはユーザーの詳細情報を表示します。</p>
        {/* 後から要件が変更されても、ここにボタンやリンクを追加できて柔軟に対応することが可能 */}
      </div>
      <div>
        <small>2025/03/15</small>
      </div>
    </Card>
  );
};

Layout コンポーネントの例

❌ Bad

以下の例では、Layout コンポーネントが title、userName、notifications、menuItems、footerText など多くのの props を受け取り、自身の内部で具体的なレイアウトやロジックまで持ってしまっています。

このような設計の場合、Layout コンポーネントが持つべき責務を超えてしまっています。

このような場合、Layout コンポーネントは多くの props を受け取ることになり、props のバケツリレーが発生しやすくなります。

const Layout = ({ title, userName, notifications, menuItems, footerText }) => {
  return (
    <div>
      <header>
        <h1>{title}</h1>
        <div>
          <span>{userName}</span>
          {notifications.map(item => (
            <span key={item.id} onClick={() => console.log(`通知: ${item.id}`)}>
              {item.text}
            </span>
          ))}
        </div>
      </header>
      <div>
        <aside>
          {menuItems.map(item => (
            <button key={item.id} onClick={() => console.log(`メニュー: ${item.id}`)}>
              {item.label}
            </button>
          ))}
        </aside>
        <main>
          <h2>メインコンテンツ</h2>
        </main>
      </div>
      <footer>{footerText}</footer>
    </div>
  );
};


✅ Good

Layout コンポーネントはレイアウトの枠組みのみを定義するようにして、具体的な中身は外から ReactNode として渡すように変更してみます。

これによって、Layout は本来の責務である“レイアウト構造の定義”だけに集中できるようになります。
また、表示する中身は親コンポーネントから自由に組み替えられるため、柔軟性・拡張性の面でも向上させることができました。

const Layout = ({ header, aside, main, footer }) => {
  return (
    <div>
      {header}
      <div>
        {aside}
        {main}
      </div>
      {footer}
    </div>
  );
};

const App = () => (
  <Layout
    header={<Header />}
    aside={<Aside />}
    main={<Main />}
    footer={<Footer />}
  />
);


まとめ

以上のように、Reactにおけるpropsのバケツリレー問題は、不要な中間コンポーネントの削除や柔軟なコンポジションパターンの活用によって大幅に解消できます。
複雑なprops設計をしてしまう原因の一つとしては、childrenをうまく使えていなかったり、propsに渡せる値のイメージが狭くなってしまっていることなどもあると思います。

常に以下のようなポイントを意識しながら開発をしていくことが大切だと思います。

  • props を渡しているだけの不必要なコンポーネントはないか?
  • 渡しているpropsはコンポーネント名に合ったものか?
  • コンポーネントの責務は適切か?
  • コンポジションパターンを活用できないか?
GitHubで編集を提案

Discussion