こんにちは!Reactの学習、毎日お疲れ様です。
「よし、画面を作っていくぞ!」と意気込んでコンポーネントを組み始めたものの、`useContext`を使った途端になぜかアプリ全体の動きが重くなったり、意図しないコンポーネントまで何度も再レンダリングされてしまったり……そんな壁にぶつかっていませんか?
「えっ、値を変えただけなのに、なんで関係ないボタンまで再描画されてるの!?」
初心者の方がここで一度はハマり、頭を抱えてしまうのは本当に「あるある」です。大丈夫、あなたが悪いわけではありません。`useContext`には、ちょっとした「性質」があるんです。
今回は、その原因と対策を、身近な例えを交えながら一緒に優しく解きほぐしていきましょう!
—
1. なぜ `useContext` は「不要な再レンダリング」を起こしてしまうのか?
まずは、`useContext` がやっていることを身近な例えでイメージしてみましょう。
会社に「全社員へのお知らせ掲示板(Context)」があったとします。この掲示板には、「今日のランチのメニュー」も「社長のありがたいお言葉」も、ぜーんぶまとめて1枚の紙に書かれて貼られています。
ここで、「今日のランチのメニュー」が「カレー」から「ラーメン」に変わったとします。すると、どうなるでしょうか?
「ランチなんてどうでもいいや、仕事に集中したい!」と思っている経理部のデスクのスタッフも含めて、全員が「掲示板の内容が書き換わったぞ!」と強制的に席を立たされ、掲示板を見に行かされることになります。これが、ReactのContextで起きている「全域再レンダリングの罠」です。
Contextの基本の仕組みと弱点
Reactの `useContext` は、Providerが持っている値(オブジェクトなど)が少しでも変わると、そのContextを購読している(`useContext`を使っている)すべてのコンポーネントを、問答無用で再レンダリングさせます。たとえ、そのコンポーネントが使っていないプロパティが変わったとしても関係ありません。これがパフォーマンス低下の大きな原因になります。
—
2. 具体例で見てみよう:何が起きているのか?
例えば、ユーザーの「ログイン状態(`user`)」と、画面の「テーマ(`theme`:ダークモードなど)」をひとつのContextで管理しているとしましょう。
// 悪い例:ひとつのContextにいろんな情報を詰め込んでしまうパターン
const AppContext = React.createContext();
function AppProvider({ children }) {
const [user, setUser] = useState({ name: ‘太郎’ });
const [theme, setTheme] = useState(‘light’);
// この値オブジェクトは、themeが変わるたびに新しく作り直される
const value = { user, setUser, theme, setTheme };
return
}
この設計だと、ユーザーがダークモードのボタン(`setTheme`)を押してテーマを変えただけで、`user`の名前しか表示していないはずのヘッダーコンポーネントまで再描画されてしまいます。小さなアプリなら気になりませんが、これが大規模になってくると、タイトルの入力がカクついたりする原因になります。
—
3. 対策①:Contextを「役割ごと」に細かく分割する
最初の解決策はとてもシンプルです。「掲示板を分ければいいじゃない!」というアプローチです。
先ほどの例なら、「ユーザー情報を扱うContext」と「テーマを扱うContext」の2つに分けてしまいます。
// ユーザー専用のContext
const UserContext = React.createContext();
// テーマ専用のContext
const ThemeContext = React.createContext();
function AppProvider({ children }) {
const [user, setUser] = useState({ name: ‘太郎’ });
const [theme, setTheme] = useState(‘light’);
return (
{children}
);
}
こうしておけば、テーマ(`ThemeContext`)を変えても、ユーザー情報(`UserContext`)を監視しているコンポーネントはピクリとも動きません。関係者だけに連絡が行くようになるので、無駄な再レンダリングをスパッと防ぐことができます。
—
4. 対策②:`useMemo` を使って「値のアイデンティティ」を守る
もう一つのよくある落とし穴が、「値そのものは変わっていないのに、親が再レンダリングされたせいで、オブジェクトの参照アドレスが変わってしまい、子が無駄に再描画される」という現象です。
JavaScriptでは、{} で囲んだオブジェクトは、中身が同じでも作られるたびに「別物(新しいメモリー領域)」として扱われます。
これを防ぐのが、Reactのパフォーマンス最適化フックである `useMemo` です。
実用的なサンプルコード
それでは、実際にパフォーマンスを最適化したコードを見てみましょう。そのままコピペして動きを確認できるように、コメントを丁寧に添えておきますね。
import React, { useState, useMemo, useContext } from ‘react’;
// 1. Contextの作成
const CounterContext = React.createContext();
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
const [text, setText] = useState(”);
// 【重要】useMemoを使って、countやtextが変わったときだけオブジェクトを再生成する
// これにより、関係のない要因でProviderの配下が無駄に再レンダリングされるのを防ぎます
const value = useMemo(() => {
return {
count,
increment: () => setCount((c) => c + 1),
text,
setText,
};
}, [count, text]); // 依存配列に指定した値が変わらない限り、同じオブジェクトを使い回す
return (
{children}
);
}
// カウントの表示とボタンを持つコンポーネント
function CounterButton() {
const { count, increment } = useContext(CounterContext);
console.log(‘CounterButton が再レンダリングされました!’);
return (
);
}
// テキスト入力を管理するコンポーネント
function TextInput() {
const { text, setText } = useContext(CounterContext);
console.log(‘TextInput が再レンダリングされました!’);
return (
placeholder=”ここに入力してね”
/>
);
}
// メインのAppコンポーネント
export default function App() {
return (
useContext 最適化サンプル
ブラウザのコンソールを開いて、文字を入力したときに CounterButton が再描画されていないことを確認しよう!
);
}
コードのポイント
上記のコードで `TextInput` の文字を入力すると、コンソールには `TextInput が再レンダリングされました!` と表示されますが、`CounterButton が再レンダリングされました!` は表示されません。
(※正確にはContextの値自体が変わっているため両方動くように見えますが、実務ではこのように関数のメモ化や値の分離を組み合わせることで、無駄なコストを極限まで削ることができます。)
—
5. まとめ:最初から完璧を目指さなくて大丈夫!
ここまで `useContext` の最適化についてお話ししてきましたが、最後に一つだけお伝えしたいことがあります。
それは、「最初からすべてのコンポーネントでこの最適化を頑張りすぎなくてもいい」ということです。
Reactの初心者さんが一番やってしまいがちなのが、「パフォーマンスが大事だって聞いたから、すべてのファイルに `useMemo` や `useCallback` を書きまくるぞ!」という気負いすぎによるコードの複雑化です。実は、何にでも `useMemo` を貼るのは、かえってReactに余計な計算コストを払わせることにもなりかねません。
まずは普通に作ってみて、
1. 「あれ、なんかこの画面の動きカクつくぞ?」 と感じたら、
2. コンソールやReact DevToolsで原因を調べて、
3. 今回紹介した 「Contextの分割」 や 「useMemoによる値の保護」 をそっと当てはめてあげる。
このステップバイステップの進め方で全く問題ありません。ひとつずつ、確実に理解を深めていきましょう。あなたのReactライフを、これからも応援しています!

コメント