こんにちは、かつコーチです。
前回は「useRefの使い方:DOM参照と値の保持」を解説しました。
今回は、コンポーネントの階層が深くなったときに必ずと言っていいほど直面する「Props drilling」問題と、それを解消する useContext を扱います。
Props drillingとは?
中間コンポーネントを素通りするだけのprops
Props drilling(プロップスドリリング)とは、親コンポーネントが持つデータを、深い階層の子コンポーネントに渡すために、間のコンポーネントが使いもしないpropsを延々とバケツリレーする状態のことです。
type User = { name: string };
function App() {
const user: User = { name: "かつコーチ" };
return <Layout user={user} />;
}
function Layout({ user }: { user: User }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }: { user: User }) {
return <UserCard user={user} />;
}
function UserCard({ user }: { user: User }) {
return <p>ようこそ、{user.name}さん</p>;
}
user を実際に使っているのは一番深い UserCard だけなのに、Layout と Sidebar は中身を使わないまま user を受け取って渡しているだけです。
なぜ問題になるのか
階層が2〜3個ならまだ許容できますが、実際のアプリでは5階層、10階層になることも珍しくありません。
こうなると、中間コンポーネントの型定義を変えるだけでも全階層を修正する羽目になり、保守性が一気に下がります。
私が以前担当した管理画面でも、ユーザー権限の情報を6階層下のボタンコンポーネントに渡すためだけに、途中の5つのコンポーネントすべてにpropsを追加する修正が必要になり、レビューのたびに「このpropsは本当に必要?」と聞かれる状態になっていました。
useContextの基本
Contextを作成して提供する
useContext は、Context(コンポーネントツリー全体で共有できるデータの入れ物)から値を取り出すためのフックです。
まずは createContext でContextを作成します。
import { createContext } from "react";
type User = { name: string };
export const UserContext = createContext<User | null>(null);
次に、値を提供したい範囲を UserContext.Provider で囲みます。
function App() {
const user: User = { name: "かつコーチ" };
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
);
}
Contextから値を取り出す
Provider の内側であれば、どの深さのコンポーネントからでも useContext で直接値を取り出せます。
import { useContext } from "react";
function Layout() {
return <Sidebar />;
}
function Sidebar() {
return <UserCard />;
}
function UserCard() {
const user = useContext(UserContext);
if (!user) return null;
return <p>ようこそ、{user.name}さん</p>;
}
Layout と Sidebar からは user に関するpropsが完全に消え、必要な UserCard だけが useContext(UserContext) で直接値を取得しています。
中間コンポーネントを一切経由せずに値を届けられるのが useContext の強みです。
つまずきやすいポイント:Providerの外で使ってエラーになる
かつコーチが実際にハマった例
私が最初に useContext を使ったとき、Providerで囲む範囲を間違えて、値が null のまま扱われてしまうバグに遭遇しました。
❌ Before:Providerの外側でuseContextを呼んでいる
function App() {
return (
<div>
<UserCard />
<UserContext.Provider value={{ name: "かつコーチ" }}>
<Layout />
</UserContext.Provider>
</div>
);
}
UserCard は UserContext.Provider の外側に置かれているため、useContext(UserContext) は createContext の第一引数で渡したデフォルト値(この例では null)しか受け取れません。
「値が渡っているはず」と思い込んでコードを書き進めた結果、user.name を読もうとしてエラーになり、原因を探すのに時間がかかりました。
✅ After:値を使うコンポーネントを必ずProviderの内側に置く
function App() {
return (
<UserContext.Provider value={{ name: "かつコーチ" }}>
<div>
<UserCard />
<Layout />
</div>
</UserContext.Provider>
);
}
UserContext.Provider で囲む範囲を、値を使う可能性のあるコンポーネントすべてを含むように広げることで解決しました。
Contextを使うときは、「どこまでをProviderで囲むか」を先に設計しておくと、こうした事故を防げます。
応用:カスタムフックでContextを安全に使う
null チェックを毎回書かなくて済むようにする
useContext をそのまま各コンポーネントで呼ぶと、user が null かもしれない前提のチェックを毎回書く必要が出てきます。
カスタムフックにまとめると、利用側のコードがすっきりします。
import { createContext, useContext } from "react";
type User = { name: string };
const UserContext = createContext<User | null>(null);
export function useUser(): User {
const user = useContext(UserContext);
if (!user) {
throw new Error("useUserはUserContext.Providerの内側で使ってください");
}
return user;
}
function UserCard() {
const user = useUser();
return <p>ようこそ、{user.name}さん</p>;
}
Providerの外で使われた場合はその場で分かりやすいエラーを投げるようにしておくと、「値がnullのまま気づかずバグになる」状態を未然に防げます。
useState や useReducer と組み合わせて、更新関数もまとめてContextで提供するのもよくあるパターンです。
まとめ
この記事のポイント
- Props drillingとは、中間コンポーネントが使わないpropsをバケツリレーしてしまう状態
createContextとuseContextを組み合わせると、階層を飛び越えて値を渡せる- 値を使うコンポーネントは必ず
Providerの内側に置く必要がある - カスタムフックにまとめてnullチェックを一元化すると、利用側のコードが安全になる
次に読むべき記事
次回は「useMemoで無駄な再計算を防ぐ」です。
パフォーマンスを意識したフックの使い方に進んでいきましょう。
→ 次の記事:useMemoで無駄な再計算を防ぐ