【実務・中級編】 useStateの遅延初期化(Lazy Initialization) – React実践ガイド

やあ、調子はどうだい?
日々、機能開発やコンポーネントのパフォーマンスチューニングに追われてご苦労さま。

今日もコードレビューをしていて、ふと気になる書き方を見かけたんだ。おそらく君も、いや、世の中の多くのReact開発者が一度はやってしまいがちな「罠」なんだけどさ。

例えば、コンポーネントの中でこんな風に書いていないかい?

// ⚠️ 毎回重い計算が走ってしまうアンチパターン
const [state, setState] = useState(heavyComputation());

一見、何の問題もない普通のコードに見えるよね。でも、フロントエンド・スペシャリストの視点から言わせてもらうと、この書き方は「毎回のレンダリングのたびに、裏で重い処理を無駄に何回も実行させ続ける時限爆弾」を抱えているようなものなんだ。

今回は、この無駄なコストを華麗に回避し、実務の現場で即座に使える「useStateの遅延初期化(Lazy Initialization)」について、ブラウザの裏側の動きも含めて徹底的に解説していこう。

—

なぜ、先のコードは「バグの温床」になり得るのか?

まずは、Reactが裏側でどう動いているのかを理解する必要がある。
JavaScript(そしてReact)の基本原則として、関数が呼び出された時、その引数にある式は「関数が実行される前に必ず評価(実行)される」というルールがある。

先のコード:

const [state, setState] = useState(heavyComputation());

これ、どういうことか分かるかい?
Reactが初回レンダリング(Mount)する時はもちろん、ユーザーの入力や親からのprops変更によってコンポーネントが再レンダリング(Re-render)されるたびに、`heavyComputation()` が毎回必ず実行されているんだよ。

「いやいや、`useState`に渡した初期値なんだから、初回だけでしょ?」って思いがちなんだけど、大間違い。JavaScriptの言語仕様上、`useState`という関数に引数を渡している時点で、その引数の評価(つまり重い計算)は毎回問答無用で実行されている。Reactはその計算結果を受け取るだけで、2回目以降のレンダリングではその結果を「単に無視している」だけなんだ。

もしその `heavyComputation()` の中で、大量のデータパースや、複雑なアルゴリズム、あるいはローカルストレージへの同期アクセスでもやっていようものなら……。想像しただけで、ブラウザのメインスレッドが悲鳴を上げるのが聞こえてこないかい?

—

救世主:「遅延初期化(Lazy Initialization)」とは何か

この問題を一発で解決するのが、今回本題である「遅延初期化」だ。
やり方は驚くほどシンプル。`useState`の引数に「値そのもの」を渡すのではなく、「初期値を返す関数(イニシャライザ)」を渡すだけ。

こう書くんだ。

// 模範解答:関数を渡すことで初回のみ実行させる
const [state, setState] = useState(() => heavyComputation());

これだけで何が変わるのか?
Reactは、`useState`の引数に渡されたのが「値」ではなく「関数」である場合、初回レンダリング(Mount時)の時だけその関数を実行し、その戻り値を初期値として採用する。そして、2回目以降の再レンダリング時には、その関数を一切実行せず、メモリ上に保持している状態の値をそのまま返すんだ。

ブラウザの実行コンテキストの視点で見ても、無駄なCPUサイクルを消費しないため、JITコンパイラやガベージコレクタにとっても非常に優しい、エレガントなコードになる。

—

【実務編】コピペで使える!実践的なユースケース

理屈は分かったところで、現場でよくある具体的なユースケースを見ていこう。
今回は、「localStorageから初期データを安全かつ効率的に読み込むコンポーネント」を例にする。`localStorage.getItem` は同期処理であり、かつメインスレッドをブロックしうる重い処理の一つだから、遅延初期化の好例だ。

以下のコードをそのままエディタに貼り付けて挙動を確認してみてほしい。

import React, { useState } from ‘react’;

// 【シミュレーション用】めちゃくちゃ重い初期化処理(例:localStorageからのパース)
const getInitialUserSettings = () => {
console.log(‘🔥 【重い処理】localStorageへのアクセスとJSONパースを実行中…’);

// 実際には localStorage.getItem(‘userSettings’) などを想定
const savedSettings = {
theme: ‘dark’,
notifications: true,
fontSize: 16,
};

// わざと重い処理に見せかけるためのダミーウェイト(実務では不要)
const start = performance.now();
while (performance.now() – start < 50) { // 50msブロックする重い計算 } return savedSettings; }; export const UserProfileSettings = () => {
// 🌟 ここがポイント!
// 関数を渡しているため、この重い処理は初回マウント時(最初の1回)しか走らない。
const [settings, setSettings] = useState(() => {
return getInitialUserSettings();
});

const [count, setCount] = useState(0);

const handleToggleTheme = () => {
setSettings((prev) => ({
…prev,
theme: prev.theme === ‘dark’ ? ‘light’ : ‘dark’,
}));
};

return (

ユーザー設定パネル

現在のテーマ: {settings.theme}

{/ 状態を更新して再レンダリングを発生させるボタン /}

※「テーマを切り替える」や「再レンダリング強制」を押しても、
コンソールに「【重い処理】」が出力されないことを確認してください。

);
};

このコードの優れている点

1. 初回のみの実行担保:
「テーマを切り替える」ボタンや「再レンダリング強制」ボタンを何度ポチポチ押しても、`getInitialUserSettings` 内の `console.log` は2回目以降一切出力されない。つまり、無駄なコストが完全に排除されている。
2. 保守性の高さ:
初期値の算出ロジックがコンポーネントの外側(あるいは関数内)にカプセル化されているため、テストもしやすいし、コンポーネント本体の可読性も落ちない。

—

シニアから後輩へのアドバイス:いつ使うべきか?

何でもかんでも `useState(() => …)` と書けばいいというわけじゃない。ここを勘違いすると、かえってコードが冗長になることもある。

以下の条件に当てはまる時だけ、この「遅延初期化」を思い出して積極的に採用してほしい。

  • `localStorage` や `sessionStorage` などのストレージ同期読み込みを行うとき
  • 初期値の生成に、巨大な配列の生成・フィルタリング・ソートなどの重い計算が絡むとき
  • DOMの計測(`getBoundingClientRect`など)を初期値に絡めざるを得ない例外的なケース

逆に、ただのプリミティブな値(`useState(0)` や `useState(”)`、簡単なオブジェクトリテラル `useState({ name: ” })` など)であれば、JavaScriptの評価コストは微小なため、わざわざアロー関数で囲む必要はない。無駄なボイラープレートコードが増えるだけだからね。

—

まとめ

フロントエンドのパフォーマンスチューニングとは、突き詰めれば「いかに無駄な計算とレンダリングをさせないか」という執念の積み重ねだ。

今日学んだ `useState` の遅延初期化は、コードの行数をほとんど増やさずに、アプリの初期表示や再レンダリング時の無駄な負荷を削ぎ落とせる非常に費用対効果の高いテクニック。

明日からのコードレビューや自社プロダクトの開発で、もし `useState(heavyFunction())` なんていうコードを見つけたら、「お、ここに時限爆弾があるぞ」とニヤリとして、サクッと遅延初期化にリファクタリングしてあげてくれ。

君ならできるさ。それじゃ、また次の現場でお会いしよう!

コメント

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