【React】useContextでProps drillingを解消する

JavaScript

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

前回は「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 だけなのに、LayoutSidebar は中身を使わないまま 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>;
}

LayoutSidebar からは 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>
  );
}

UserCardUserContext.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 をそのまま各コンポーネントで呼ぶと、usernull かもしれない前提のチェックを毎回書く必要が出てきます。

カスタムフックにまとめると、利用側のコードがすっきりします。

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のまま気づかずバグになる」状態を未然に防げます。

useStateuseReducer と組み合わせて、更新関数もまとめてContextで提供するのもよくあるパターンです。

まとめ

この記事のポイント

  • Props drillingとは、中間コンポーネントが使わないpropsをバケツリレーしてしまう状態
  • createContextuseContext を組み合わせると、階層を飛び越えて値を渡せる
  • 値を使うコンポーネントは必ずProviderの内側に置く必要がある
  • カスタムフックにまとめてnullチェックを一元化すると、利用側のコードが安全になる

次に読むべき記事

次回は「useMemoで無駄な再計算を防ぐ」です。

パフォーマンスを意識したフックの使い方に進んでいきましょう。

→ 次の記事:useMemoで無駄な再計算を防ぐ

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