Reactで余計な再レンダリングを防ぐ方法(useCallback / React.memo)
Reactで余計な再レンダリングを防ぐ方法(useCallback / React.memo)

Reactで余計な再レンダリングを防ぐ方法(useCallback / React.memo)

Reactの実装でエンジニアが意識すべき点の一つに、 「余計な再レンダリングを防ぐ」 というパフォーマンスチューニングがあります。 再レンダリングが頻発すると画面の描画負荷(メインスレッドのブロック)が高まり、ユーザーの操作性を損なう原因になるため、レンダリングは必要最低限に抑えるのが理想です。

余計な再レンダリングが発生する理由

Reactは「状態(State)が変化した時」に画面(DOM)を更新します。動的なUIを作る上でレンダリング自体は必須ですが、問題なのは「変更が必要ない箇所まで巻き添えで再レンダリングされる」ケースです。

特に多いのが以下のパターンです。

・親コンポーネントが再レンダリングされると、子コンポーネントも無条件で再レンダリングされる

・値(Props)の実質的な中身は変わっていないのに、「新しいデータ」として検知されてしまう

逆に言えば、この「巻き添え」と「誤検知」を防げば、レンダリングコストを大幅に削減できます。

useCallbackとReact.memoが基本セット

この問題を解決するための代表的なフック・関数が useCallbackReact.memo です。

・React.memo:コンポーネントをメモ化する。渡されたPropsに変更がなければ、親が再レンダリングされても、このコンポーネントは再レンダリングしない。

・useCallback:関数定義をメモ化する。依存配列が変わらない限り、再レンダリング前と同じ関数(同じ参照)を使い回す。

重要なのは、これらは「セットで使う」ケースが多いということです。

なぜセットで使う必要があるのか?

Reactの仕組み上、コンポーネント内で定義した関数は、再レンダリングのたびに作り直され、新しいメモリ番地(参照)」が割り当てられます。

もし React.memo だけを使っていても、Propsとして渡される関数が毎回「別人(新しい参照)」になってしまうため、Propsが変更されたとみなされ、再レンダリングを防げません。

【処理の流れイメージ】
1.setTitle('新しいタイトル') を実行(State更新)

2.親コンポーネントが再レンダリング開始

3.useCallback → 関数(handleTitleChange)は再作成されず、前回のものを維持

4.React.memo → 子コンポーネントは「Props(関数)が変わっていない」と判断

5.結果: 関係のない子コンポーネントの再レンダリングがスキップされる

具体的な使い分けパターン

基本はセットですが、Propsの中身によって使い分けます。

1. 関数をPropsとして渡す場合 → 両方必要

// 親コンポーネント
// 関数をuseCallbackでラップして、参照を固定する
const handleClick = useCallback(() => {
  console.log("Clicked");
}, []); 

return <child onclick="{handleClick}">;

// 子コンポーネント
// React.memoでPropsの変更を監視する
const Child = memo(({ onClick }) => {
  console.log("Child Rendered");
  return <button onclick="{onClick}">Click me</button>;
});</child>


ここで useCallbackを忘れると、handleClickが毎回作り直されるため、ChildReact.memo は意味をなさなくなります。

2. 関数以外のオブジェクト・配列を渡す場合 → useMemo + React.memo

関数と同様に、オブジェクトや配列も毎回新しい参照として生成されるため、useMemo で値を固定する必要があります。

// 親コンポーネント
const data = useMemo(() => ({ name: 'test' }), []); // 値自体をメモ化
return <child data="{data}">;

// 子コンポーネント
const Child = memo(({ data }) => {});</child>

実装デモ(State管理とメモ化)

以下は、記事投稿画面を想定した「両方セット」の実装例です。

// 親(BlogPage)
// ハンドラー関数をuseCallbackでメモ化
const handleTitleChange = useCallback((newTitle: string) => {
  setTitle(newTitle);
}, []);

const handleFileChange = useCallback((file: File | null) => {
  setSelectedFile(file);
}, []);

return (
  <>
    {/* 各子コンポーネントにメモ化した関数を渡す */}
    <articlesettings title="{title}" onchangetitle="{handleTitleChange}" onchangefile="{handleFileChange}">
    <articlepage content="{htmlContent}" onchangecontent="{setHtmlContent}">
  
);

// 子(ArticleSettings)
// React.memoでラップして、Propsが変わらない限り再レンダリングを防ぐ
const ArticleSettings = memo(({ title, onChangeTitle, onChangeFile }) => {
  // ...実装
});</articlepage></articlesettings>

まとめ

動的な更新が多いアプリケーションでは、useCallbackReact.memo を組み合わせたチューニングが重要になります。

ただし、以下の点には注意が必要です。

・コードが複雑になる:ボイラープレート(定型コード)が増え、可読性が下がる可能性があります。

・早すぎる最適化は避ける:小規模なアプリや、再レンダリングされても一瞬で終わるような軽いコンポーネントであれば、無理にメモ化する必要はありません。

まずは機能を実装し、「動作が重い」「プロファイラで無駄なレンダリングを確認した」という段階で導入することをおすすめします。

関連記事