やあ。現場のコードレビューで「とりあえずContextに入れとけばいいか」という安易な実装を見て、思わず天を仰いだことはないかい?
中級の壁を抜け出し、シニアへとステップアップしようとしている君なら、Context APIが「魔法のグローバル変数入れ」ではないことは薄々感づいているはずだ。Reactのレンダリングメカニズムを理解せずになんでもかんでもContextにつっこむと、アプリが肥大化した瞬間に「ちょっと文字を入力しただけでツリー全体が激重再描画される地獄」が幕を開ける。
今回は、実務で本当に使える、パフォーマンスと保守性を両立した「正しい`useContext`によるProviderの構築パターン」を徹底的に解説しよう。
—
なぜContextを使うのか?そして裏側で何が起きているのか
まず、ブラウザとReactの裏側の動きを整理しておこう。
Reactにおいて、状態が更新されると何が起きるか?そう、その状態を保持しているコンポーネント、およびその配下のサブツリーがすべて再レンダリング(Re-render)の対象になる。
プロパティバケツリレー(Prop Drilling)を防ぐためにContextを使うわけだが、ここで大きな勘違いをしがちなのが「Contextの値が変われば、それを購読している全コンポーネントが無条件で再描画される」という事実だ。
ブラウザのメインスレッドでこれがどう処理されるか。JSの実行時間が長引けば長引くほど、UIの入力遅延(Jank)が発生し、ユーザーは「このアプリ、なんかモッサリしててイライラするな」と感じる。だからこそ、Providerの設計と状態の分離が、シニアの腕の見せ所になるというわけだ。
—
現場で即戦力になる!堅牢なProvider設計の3カ条
実務で破綻しないContextを組むために、以下の3つを鉄則として覚えておいてほしい。
1. 状態(State)と更新関数(Dispatcher)を分離する
値とそれを変更する関数を一つのオブジェクトに混ぜて入れるな。これやると、状態だけをちょっと見たいだけのコンポーネントまで、関数が再生成されるたびに(あるいは値が変わるたびに)再描画されるハメになる。
2. カスタムHookでラップし、Provider外での利用をガードする
`useContext(MyContext)`を直接コンポーネントから呼ばせるな。必ず専用のカスタムHookを作り、Providerの外で呼ばれたら即座にエラーを吐かせろ。
3. `useMemo`でバリューをメモ化する
Providerのバリューをオブジェクトリテラルでそのまま渡すと、親が再描画されただけで子も連鎖的に再描画される。
—
実装例:認証状態(Auth)を管理する堅牢なProvider
言葉だけではピンとこないだろう。実務でそのまま使える、ユーザーの認証状態を管理するContextの実装コードを見てほしい。エディタにコピペして挙動を確認できるように、丁寧にコメントを入れておいた。
import React, { createContext, useContext, useState, useMemo, useCallback } from ‘react’;
// 1. 扱うデータの型定義を明確に行う(TypeScriptの恩恵を最大限に受けるため)
interface User {
id: string;
name: string;
email: string;
}
interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
}
interface AuthActionsType {
login: (userData: User) => void;
logout: () => void;
}
// 2. 状態とアクションのContextをあえて分離する(ここがパフォーマンスチューニングの肝!)
const AuthStateContext = createContext
const AuthActionsContext = createContext
// 3. Providerコンポーネントの作成
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState
// ログイン処理:useCallbackで関数の参照を安定させる
const login = useCallback((userData: User) => {
// 実際のアプリではここでAPI通信やトークン保存を行う
setUser(userData);
}, []);
// ログアウト処理
const logout = useCallback(() => {
setUser(null);
}, []);
// 状態のメモ化:userが変わった時だけオブジェクトを再生成する
const authState = useMemo(() => ({
user,
isAuthenticated: user !== null,
}), [user]);
// アクションのメモ化:これらは基本的に変わらないので空の依存配列でOK
const authActions = useMemo(() => ({
login,
logout,
}), [login, logout]);
return (
{children}
);
};
// 4. 安全装置付きのカスタムHookを定義する
// 状態を読むだけのHook
export const useAuthState = (): AuthContextType => {
const context = useContext(AuthStateContext);
if (context === undefined) {
// Providerの外で呼ばれたらバグとして即座に検知できるようにする
throw new Error(‘useAuthState must be used within an AuthProvider’);
}
return context;
};
// アクションを実行するだけのHook
export const useAuthActions = (): AuthActionsType => {
const context = useContext(AuthActionsContext);
if (context === undefined) {
throw new Error(‘useAuthActions must be used within an AuthProvider’);
}
return context;
};
このコードの美しいポイント
- 関心の分離: `useAuthState`(読む用)と `useAuthActions`(書く用)を分けたことによって、「ボタンをクリックしてログアウトさせたいだけ(画面の再描画は不要)」のコンポーネントが、ユーザー名の変更によって再描画されるのを完璧に防いでいる。
- フェイルファスト(Fail-fast): カスタムHook内で `undefined` チェックを行い、エラーをスローさせている。これにより、「なぜか値が取れない」というよくあるミスを開発段階で秒速で見つけられる。
—
現場からのアドバイス:なんでもContextに入れるな
最後に、シニアとしての苦言を呈しておこう。
世の中には「ReduxめんどいからContextでいいや」といって、アプリ全体のサーバーキャッシュ、フォームの入力値、UIの開閉状態まで、あらゆるものを1つのContextにブチ込むエンジニアがいる。
それはアンチパターンだ。
頻繁に変わる状態(例えばマウスの座標や、入力フォームの一文字一文字)をContextに入れると、それを購読しているツリー全体のパフォーマンスが死ぬ。サーバーから取得したデータ管理には `TanStack Query` や `SWR` を使い、純粋なUIのグローバル状態やテーマ、認証といった「めったに変わらない、あるいはアプリ全体で共通のコンテキスト」だけにContextを絞るのが、モダンなReact開発の黄金律だ。
さあ、この知見を武器に、君のプロジェクトのコードベースをより美しく、より爆速にブラッシュアップしてくれ。期待しているよ!

コメント