Reactでネストしたコンポーネントにデータを渡す方法を完全解説!Context APIで深い階層も簡単に
生徒
「コンポーネントが何階層も深くなっているとき、一番下の子コンポーネントにデータを渡すにはどうすればいいんですか?propsをずっと渡し続けるしかないですか?」
先生
「propsを何段も渡し続けるのはProps drillingという問題で、コードが複雑になってしまいます。Context APIを使えば、ネストがどれだけ深くても直接データを届けられますよ。」
生徒
「どんな場面で使えばいいのか、実際のコードで見せてもらえますか?」
先生
「もちろんです。propsとContext APIの両方を比べながら、具体的に確認していきましょう!」
1. ネストしたコンポーネントとは?まず構造を理解しよう
Reactでアプリを作るとき、画面の各部品を「コンポーネント」という単位に分けて組み合わせます。コンポーネントの中にさらにコンポーネントを入れることをネストといいます。日本語では「入れ子」とも呼びます。
たとえばブログサイトであれば、ページ全体を管理するAppコンポーネントの中にHeaderコンポーネントがあり、その中にNavコンポーネント、さらにその中にNavItemコンポーネントがある、というように階層が深くなっていきます。
このようにネストが深くなった構造で、一番上の親コンポーネントが持つデータを一番深い子コンポーネントに渡したいとき、どうすればよいかが大きな課題になります。素直にpropsを使う方法と、Context APIを使う方法の2通りがあるので、それぞれの特徴を理解しておきましょう。
2. propsでネストしたコンポーネントにデータを渡す書き方
まず、propsを使って深い階層のコンポーネントにデータを渡す基本的な書き方を見てみましょう。propsとは、親コンポーネントから子コンポーネントへデータを渡すための仕組みです。
import React from "react";
function App() {
const siteName = "React学習サイト";
// HeaderにpropsとしてsiteNameを渡す
return <Header siteName={siteName} />;
}
function Header({ siteName }) {
// HeaderはsiteNameをNavに渡すだけ(自分では使わない)
return <Nav siteName={siteName} />;
}
function Nav({ siteName }) {
// NavもsiteNameをNavItemに渡すだけ
return <NavItem siteName={siteName} />;
}
function NavItem({ siteName }) {
// ここでようやくsiteNameを使う
return <p>サイト名:{siteName}</p>;
}
export default App;
2〜3階層程度であればpropsでも十分ですが、階層が5段、6段と深くなったり、渡すデータが増えたりすると、コードの読みやすさが急激に下がっていきます。このような状況を解決するのがContext APIです。
3. Context APIを使ってネストしたコンポーネントに直接データを届ける
Context APIを使えば、途中のコンポーネントを経由せずに、深い階層のコンポーネントへ直接データを届けることができます。先ほどの例をContext APIで書き直してみましょう。
import React, { createContext, useContext } from "react";
// Contextを作成する
const SiteContext = createContext("");
function App() {
const siteName = "React学習サイト";
return (
// Providerで囲んでvalueにデータをセット
<SiteContext.Provider value={siteName}>
<Header />
</SiteContext.Provider>
);
}
function Header() {
// siteNameを受け取らなくていい
return <Nav />;
}
function Nav() {
// こちらも受け取らなくていい
return <NavItem />;
}
function NavItem() {
// useContextで直接データを取り出す
const siteName = useContext(SiteContext);
return <p>サイト名:{siteName}</p>;
}
export default App;
階層がどれだけ深くても、Providerの内側であればuseContextひとつでデータを取り出せます。途中のコンポーネントはデータのことを一切知らなくてよいので、それぞれのコンポーネントが自分の役割だけに集中できます。
4. オブジェクトを渡して複数データを深い階層に届ける方法
実際のアプリでは、ひとつのデータだけでなく複数のデータをまとめて深い階層に渡したい場面がよくあります。Providerのvalueにオブジェクトをセットすることで、複数のデータを一度にまとめて渡すことができます。
オブジェクトとは、{ キー名: 値 }の形で複数のデータをひとまとめにしたデータの型のことです。名前と年齢をまとめるなら{ name: "田中", age: 30 }のように書きます。
import React, { createContext, useContext } from "react";
const ProfileContext = createContext(null);
function App() {
const profileData = {
name: "鈴木さん",
job: "エンジニア",
level: "中級",
};
return (
<ProfileContext.Provider value={profileData}>
<PageLayout />
</ProfileContext.Provider>
);
}
function PageLayout() {
return (
<div>
<Sidebar />
<MainArea />
</div>
);
}
function Sidebar() {
// Sidebarは中間コンポーネントなので何も受け取らない
return <UserCard />;
}
function UserCard() {
// 必要なデータだけ取り出す(分割代入)
const { name, job } = useContext(ProfileContext);
return (
<div class="card p-3">
<p><i class="bi bi-person-fill"></i> {name}</p>
<p><i class="bi bi-briefcase-fill"></i> {job}</p>
</div>
);
}
function MainArea() {
const { level } = useContext(ProfileContext);
return <p>スキルレベル:{level}</p>;
}
export default App;
const { name, job } = useContext(ProfileContext)のように書くと、オブジェクトから必要なプロパティだけを取り出せます。この書き方を分割代入と呼び、Reactのコードでは非常によく使われます。
5. useStateと組み合わせてネスト先からデータを更新する方法
Context APIは読み取るだけでなく、深い階層のコンポーネントからデータを変更することも可能です。useStateの状態と更新関数をまとめてContextで共有する方法が広く使われています。
useStateとは、変化する値をコンポーネント内で管理するためのReactフックです。フックとは、Reactが用意している特別な関数のことです。useStateを使うと、値が変わったときに画面が自動的に更新されます。
import React, { createContext, useContext, useState } from "react";
const NoticeContext = createContext(null);
function App() {
const [notice, setNotice] = useState("お知らせはありません");
return (
// 値と更新関数をオブジェクトにまとめてvalueに渡す
<NoticeContext.Provider value={{ notice, setNotice }}>
<PageWrapper />
</NoticeContext.Provider>
);
}
function PageWrapper() {
return (
<div>
<NoticeBoard />
<AdminArea />
</div>
);
}
function NoticeBoard() {
// noticeだけ取り出して表示する
const { notice } = useContext(NoticeContext);
return (
<div class="alert alert-primary">
<i class="bi bi-megaphone-fill"></i> {notice}
</div>
);
}
function AdminArea() {
// setNoticeを取り出してお知らせを変更できるようにする
const { setNotice } = useContext(NoticeContext);
return (
<div>
<button class="btn btn-sm btn-outline-secondary" onClick={() => setNotice("本日はメンテナンスのためサービスを停止します")}>
お知らせを更新する
</button>
</div>
);
}
export default App;
このように更新関数もContextに含めることで、アプリのどこに配置されたコンポーネントからでも状態を変更できます。状態の変更は即座にContextを使っているすべてのコンポーネントに反映されます。
6. Contextを別ファイルに切り出してネストの深いアプリを管理しやすくする
アプリが大きくなってくると、Contextの定義をAppコンポーネントの中に書き続けるのが難しくなってきます。そこで、ContextとProviderを専用の別ファイルに切り出す構成が一般的に使われています。
専用ファイルに切り出す利点は、コードの見通しがよくなることと、どこからでも同じContextをインポートして使えることです。またカスタムフックと組み合わせると、useContextを毎回書かなくてよくなります。カスタムフックとは、useContextなどのフックをラップして独自に作った関数のことで、コードの再利用性を高めます。
ファイル構成の一例としては、contexts/ThemeContext.jsxのようなフォルダにContextファイルをまとめておくやり方があります。このファイルの中でcreateContext、Providerコンポーネント、カスタムフックの3つをまとめてエクスポートしておきます。使う側のコンポーネントはインポートするだけでよいので、管理がとても楽になります。
ネストが深いアプリほど、このようなファイル分割の恩恵を感じやすいです。どのContextがどんなデータを管理しているかがファイル名で一目でわかるようになり、チーム開発でも迷わずコードを読み書きできるようになります。
7. propsとContext APIをどう使い分ければいいか
ここまでpropsとContext APIの両方を見てきました。どちらを使うか迷ったときの判断基準をまとめておきましょう。
まず、コンポーネントの階層が2〜3段程度で、渡すデータが少ない場合はpropsで十分です。propsはシンプルで直感的なため、コードが読みやすく、データの流れも追いやすいです。
一方で、次のような場合はContext APIが向いています。コンポーネントの階層が4段以上深い場合、アプリ全体で使うログイン情報やテーマ設定などを共有したい場合、途中のコンポーネントがデータをまったく使わないのにpropsを受け取り続けなければならない場合などが代表的な例です。
また、Context APIを使いすぎることにも注意が必要です。Contextの値が変わると、そのContextを参照しているすべてのコンポーネントが再描画されます。頻繁に値が変わるデータをContextに入れすぎると、パフォーマンスに影響が出ることがあります。「アプリ全体で共有する必要があるデータかどうか」を考えながら使うのが、長く使えるコードを書くコツです。