こんにちは。チームのコードレビューをしていて、最近一番「あぁ、またやってるな」と頭を抱えたくなる瞬間がどこか分かるかい?そう、「Prop Drilling(プロップドリリング)」の泥沼と、それに伴う型定義の迷走だ。
画面の深くまでデータを渡したいがために、使ってもいないPropsをひたすらバケツリレーさせ、途中のコンポーネントの型定義が `any` や `unknown` で汚染されていく……。中級からシニアへステップアップする過程で、誰もが一度は通る地獄絵図だよな。
今日は、この不毛なバケツリレーに終止符を打ち、TypeScriptの恩恵を最大限に受けながらContext APIを型安全に、かつ実務の現場でビクともしない堅牢な設計で使い倒す方法を伝授しよう。
—
なぜProp Drillingが発生し、なぜContextで解決できるのか
まず、裏側の話を少ししておこう。ReactがブラウザのDOMをどうこうする前に、JSのメモリ上で「コンポーネントツリー」という巨大なオブジェクト構造を構築している。
親から子へデータを渡すとき、Reactは単にその関数の引数(Props)として値を流し込んでいるだけだ。
階層が2〜3段なら可愛いものだが、これが7段、8段と深くなるとどうなるか。途中のコンポーネントは「自分自身は一切そのデータを使わない」にもかかわらず、親から受け取ったものをそのまま子へ横流しするために、型定義を書き、再レンダリングの波をまともに受けることになる。これはコードの結合度を無駄に高め、変更耐性を著しく落とす悪手だ。
そこで登場するのが Context API だ。
Contextは、いわば「コンポーネントツリーの裏口(あるいはワームホール)」。特定のツリー内であれば、途中のコンポーネントを完全にバイパスして、必要な下流のコンポーネントに直接データを届けられる。
しかし、ここで多くの人がつまずく。「Contextを使うと、型定義がめんどくさい」「`undefined` のハンドリングでコードが汚くなる」 とね。
それをエレガントに解決するベストプラクティスを、実際のコードベースを想定して見ていこう。
—
実務で使える!型安全なContext設計の全体像
今回は、実務でよくある「ログインユーザーの認証情報(Auth)」を全社的なスコープで管理するシチュエーションを例にする。
以下のコードをそのままエディタにコピーして動かせるように、必要な型定義、Provider、そしてカスタムフックをひとまとめにした実戦的かつ美しい構成を用意した。
import React, { createContext, useContext, useState, useMemo, ReactNode } from ‘react’;
// ==========================================
// 1. 型定義セクション
// ==========================================
// ユーザー情報のデータ型
export interface User {
id: string;
name: string;
email: string;
role: ‘admin’ | ‘editor’ | ‘viewer’;
}
// Contextが提供する値(Stateと操作関数)の型
interface AuthContextType {
user: User | null;
login: (userData: User) => void;
logout: () => void;
isAuthenticated: boolean;
}
// ProviderのProps型
interface AuthProviderProps {
children: ReactNode; // childrenはReactNodeで確実に型付けする
}
// ==========================================
// 2. Contextの作成(初期値のトリック)
// ==========================================
// ここであえて `undefined` を初期値に設定するのがポイント。
// 「Providerの外で使われたら即座にバグとして検知する」ための布石だ。
const AuthContext = createContext
// ==========================================
// 3. Providerコンポーネントの実装
// ==========================================
export const AuthProvider: React.FC
const [user, setUser] = useState
// ログイン処理
const login = (userData: User) => {
// 実際はここでAPI叩くなりトークン保存するなりする
setUser(userData);
};
// ログアウト処理
const logout = () => {
setUser(null);
};
// ログイン状態の導出プロパティ
const isAuthenticated = user !== null;
//useMemoで値のメモ化を行い、不要な再レンダリングを防止する(現場の鉄則)
const value = useMemo(
() => ({
user,
login,
logout,
isAuthenticated,
}),
[user]
);
return
};
// ==========================================
// 4. 型安全なカスタムフックの作成
// ==========================================
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
// 万が一、AuthProviderの外側でこのフックが呼ばれたら、
// 黙ってundefinedを返すのではなく、開発段階で気づけるようにエラーを投げる。
if (context === undefined) {
throw new Error(‘useAuth must be used within an AuthProvider. ちゃんとツリーの親にProviderを置いてください!’);
}
return context;
};
—
シニアが教える、この設計が「現場で勝てる」3つの理由
このコードには、ただ動くだけではない、実務を見据えたシニアのこだわりが詰まっている。
1. `createContext(undefined)` の意図
よくネットの古い記事だと、`createContext
中身が空っぽなのに「型としては存在している」と嘘をつくことになるため、Providerを置くのを忘れたバグを踏んだ際、原因不明の `TypeError: Cannot read properties of undefined` に悩まされることになる。
初期値を `undefined` にし、型も `| undefined` を許容することで、TypeScriptの型ガードを強制させよう。
2. カスタムフック内でガード節を入れる
利用側(コンポーネント)で、毎回 `const auth = useContext(AuthContext); if (!auth) { … }` と書くのは怠慢だし、コードが汚れる。
上記のように `useAuth` という専用のカスタムフックを切り出し、その中で `undefined` チェックとエラースローをカプセル化してしまうのがプロのやり方だ。
これによって、呼び出し側は `const { user, login } = useAuth();` と書くだけで、一切の `null` チェックのボイラープレートを書かずに、完全な型補完の恩恵を受けることができる。
3. `useMemo` によるパフォーマンスへの配慮
Contextの値をハックする上で忘れてはならないのが、「Providerのバリューが変わると、それを購読している(`useContext`している)子孫コンポーネントは、例え関係のないプロパティしか使っていなくても強制的に再レンダリングされる」というReactの仕様だ。
上記のサンプルでは、`value` を `useMemo` でラップし、`user` ステートが変化した時のみ新しいオブジェクト参照を生成するようにしている。アプリケーションが肥大化した際、この一手間がパフォーマンスの明暗を分ける。
—
まとめ
Prop Drillingの地獄から抜け出し、Context APIを型安全に使いこなすことは、中級からワンランク上のフロントエンドエンジニアへ駆け上がるための必須スキルだ。
今回紹介したパターンは、そのまま実務のプロダクション環境でボイラープレートとして使える構成になっている。
「とりあえず動く」コードから、「型とパフォーマンスが担保された美しい」コードへ。今日のレビューから、ぜひチームのコードベースに持ち帰って実践してみてほしい。
それじゃあ、また次の現場でお会いしよう。

コメント