【実務・中級編】 useContextの再レンダリング問題と最適化手法 – React実践ガイド

やあ。今日も今日とてコードレビューに追われてるかい?
フロントエンドの海原へ漕ぎ出し、数々のバグという名の海中要塞を突破してきた中級エンジニアのキミなら、そろそろ「Reactの裏側の動き」にモヤモヤし始める頃合いなんじゃないかと思う。

「よし、グローバルな状態管理をシンプルにするために、自前で `useContext` を組もう!」
――うん、最初はいいんだ。コードもスッキリするし、Reduxのボイラープレート地獄からも解放された気になる。だが、アプリが成長してコンポーネントツリーが肥大化した瞬間、悪夢が訪れる。
「あれ?なんでボタンを1回クリックしただけなのに、アプリ全体のフォームが再描画されて重くなってるんだ……?」

そう、これが `useContext` の罠だ。
今日は、現場で幾度となく若手から泣きつきを受け、その都度解決してきた「`useContext` の再レンダリング問題と、その泥臭くも確実な最適化手法」について、ブラウザの気持ちになりながら徹底的に紐解いていこう。

—

なぜ `useContext` は「全方位的」に再レンダリングを引き起こすのか?

まず、Reactのコアな仕様を思い出してほしい。
`useContext(MyContext)` を使ったコンポーネントは、Contextの値が更新されると、たとえ自分が参照しているプロパティが変更されていなくても、無条件で強制的に再レンダリングされる。

ブラウザの裏側の話をしよう。
Reactは、Contextのプロバイダ(``)の `value` が変わったと検知すると、そのコンテキストを購読(subscribe)しているすべてのコンポーネントのツリーを巡回し、「おい、親のデータが変わったぞ、お前も起きろ!」とばかりに再レンダリングのスケジュールを積んでいく。

ここで中級者がやりがちな、一番やってはいけないアンチパターンを見てみよう。

// 【やってはいけないアンチパターン】
import React, { createContext, useContext, useState } from ‘react’;

const AppContext = createContext();

export const AppProvider = ({ children }) => {
const [user, setUser] = useState({ name: ‘Taro’, role: ‘guest’ });
const [theme, setTheme] = useState(‘light’);

// 🔴 やってしまったな!毎回のレンダリングで新しいオブジェクトリテラルを生み出している
const value = { user, setUser, theme, setTheme };

return (

{children}

);
};

これの何がマズいか、わかるかい?
たとえ `theme` しか変更されていなくても、`AppProvider` が再レンダリングされるたびに、JavaScriptのメモリ上では全く新しいオブジェクト `{ user, setUser, theme, setTheme }` が生成される。
Reactは `Object.is` で `value` の変更を検知するから、「おっ、前のオブジェクトと参照先(メモリのアドレス)が変わってるぞ!全購読者に再レンダリングを命じなきゃ!」と勘違いしてしまうんだ。

結果、ユーザー名だけ変えたいのに、テーマカラーすら関係ないヘッダーやサイドバーまで巻き込んでDOMの差分比較が走る。これがアプリを重くするガンだ。

—

実務で使える!Context最適化の3大アプローチ

じゃあ、どうすればいいのか。現場で即座に使える実践的なアプローチを3つ伝授しよう。

1. `useMemo` で `value` の参照を死守する

まずは基本のキだ。オブジェクトの参照が変わるのを防ぐために、`useMemo` でキャッシュしよう。

import React, { createContext, useContext, useState, useMemo } from ‘react’;

const UserContext = createContext(null);

export const UserProvider = ({ children }) => {
const [user, setUser] = useState({ name: ‘Taro’, role: ‘admin’ });

// 状態と更新関数をuseMemoでメモ化し、依存配列が変わらない限り同じ参照を保つ
const value = useMemo(() => {
return { user, setUser };
}, [user]);

return (

{children}

);
};

これだけでも無駄な再レンダリングはかなり防げる。だが、これだけでは「userオブジェクトの中身が変わったときに関係ないコンポーネントまで再描画される」という問題は解決しない。そこで次のステップだ。

2. 「状態」と「更新関数」をContextで分離する

ここからがプロの技だ。Reactの更新関数(`setUser` や `useState` が返すセッター)は、コンポーネントのライフサイクルを通じて絶対に参照が変わらない(安定している)という特性がある。

これを活かして、Contextを「データ用」と「アクション(更新関数)用」の2つに分割するんだ。

import React, { createContext, useContext, useState, useMemo } from ‘react’;

// データを保持するContextと、更新関数を保持するContextを分ける
const UserStateContext = createContext(null);
const UserDispatchContext = createContext(null);

export const UserProvider = ({ children }) => {
const [user, setUser] = useState({ name: ‘Jiro’, point: 100 });

// セッターは変わらないのでuseMemoの依存配列は空でOK
const dispatch = useMemo(() => setUser, []);

return (


{children}


);
};

// 💡 現場で重宝するカスタムフックの切り出し
export const useUserValue = () => {
const context = useContext(UserStateContext);
if (!context) throw new Error(‘useUserValue must be used within a UserProvider’);
return context;
};

export const useUserDispatch = () => {
const context = useContext(UserDispatchContext);
if (!context) throw new Error(‘useUserDispatch must be used within a UserProvider’);
return context;
};

この設計の何が素晴らしいか?
「ボタンを押してデータを更新するだけ(値は表示しない)」のコンポーネントには `useUserDispatch` だけを読ませる。そうすれば、ユーザーデータが書き換わっても、そのボタンコンポーネントは一切再レンダリングされない。完璧な関心の分離だ。

3. さらに細分化するなら、カスタムフックとセレクター風アプローチ

ReduxやZustandのセレクターほどリッチではなくても、Contextの値を取り出すときに特定のプロパティだけに絞り込みたい場合がある。

// ユーザー名だけを監視するカスタムフック
export const useUserName = () => {
const user = useUserValue();
// 注意:これだけだとuser全体が変わったときに再レンダリングされるが、
// コンポーネント側でReact.memoを併用することで真価を発揮する
return user.name;
};

もし本当に大規模なアプリケーションで、Contextの値が複雑に絡み合っているなら、自前で `useContext` をこねくり回すのをやめて、素直に Zustand や Jotai などのアトミック系・シグナル系状態管理ライブラリに移行する勇気を持つことも、シニアとしての重要な決断だ。ツールには適材適所がある。

—

チーフアーキテクトからのメッセージ

パフォーマンスチューニングの鉄則は、「計測してから最適化する(Premature optimization is the root of all evil)」だ。
React DevToolsのProfilerを開いて、「実際にどこが何ミリ秒かけて再レンダリングされているか」を確認してから、今日紹介した `useMemo` や Contextの分割を適用してほしい。

「動くコード」を書くのはジュニアでもできる。しかし、「スケールしても耐えうる、キレのあるコード」を書くのは、日々のこうした泥臭い工夫の積み重ねだ。

キミならできる。次のコードレビューで、チームをアッと言わせる美しい設計を見せてくれよ!

コメント

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