やあ。最近、チームのコードレビューをしていて、`useState`の初期化や更新処理のあたりで「おや?」と首を傾げたくなる実装にぶつかることが増えてきた。
中級へのステップを駆け上がっている君たちなら、コンポーネントの中で `const [state, setState] = useState()` と書けば状態が持てることくらい、呼吸をするように分かっているはずだ。しかし、「なぜその書き方をするのか」「裏でReactがどう動いているのか」を突き詰めていくと、実務で踏み抜くバグの数が劇的に減る。
今回は、その `useState` の基本中の基本、そして「初期化」という一見地味なテーマにスポットを当てて、シニアの視点から現場の知見をガッツリ伝授しよう。
—
1. なぜ `useState` は配列を返すのか?(JavaScriptの言語的側面)
まずは基本の復習からだ。以下のコードを見てほしい。
const [count, setCount] = React.useState(0);
ここで使われているのは、JavaScriptの配列の分割代入(Destructuring assignment)だ。
オブジェクトではなく「なぜ配列なのか?」と疑問に思ったことはないだろうか。
オブジェクトの分割代入であれば、プロパティ名を指定する必要がある。例えばこんな感じだ。
// もしオブジェクトだったら…(架空のAPI)
const { state: count, updateState: setCount } = useState(0);
これだと、コンポーネント内で同じフックを何回も呼ぶときに、変数名をリネームする手間が生まれて非常にめんどくさい。
const { state: count, updateState: setCount } = useState(0);
const { state: user, updateState: setUser } = useState(null);
だるいよな。配列であれば、インデックスの順番さえ合っていれば、呼び出し側で自分の好きな変数名を自由に付けられる。
const [count, setCount] = useState(0);
const [user, setUser] = useState(null);
Reactチームのこういった「開発者のタイピング数を減らし、可読性を上げる」ための細かい気配りは、本当にリスペクトに値する。
—
2. 初期値設定の罠:静的初期化と「遅延初期化」
さて、ここからが本題だ。実務で一番差が出るのが「初期値(Initial State)をどう与えるか」という点。
例えば、初期値として「重い計算処理の結果」や「ローカルストレージからの読み込み」を行いたいとする。
やりがちなアンチパターン(毎回実行される罠)
// ❌ やりがちなダメパターン
function UserProfile({ userId }) {
// heavyCalculation() は、コンポーネントが再レンダリングされるたびに毎回実行される!
const [userData, setUserData] = useState(heavyCalculation(userId));
return
;
}
「あれ? `useState(initialValue)` って最初のマウント時にしか使われないんじゃないの?」と思ったそこの君。鋭いが、JavaScriptの仕様を思い出してほしい。
Reactがコンポーネントを再レンダリングするとき、その関数コンポーネント自体がもう一度頭から実行される。つまり、`heavyCalculation(userId)` という関数呼び出しのコード自体は、再レンダリングのたびに毎回評価(実行)されているのだ。その戻り値が捨てられているだけで、CPUやメモリの無駄遣いには変わりない。
ベストプラクティス:関数を渡す「遅延初期化(Lazy Initial State)」
重い処理や、初期化コストが高いデータを初期値にしたい場合は、`useState` に直接値を入れるのではなく、関数を渡す。これが鉄則だ。
// ⭕ 正しい遅延初期化のパターン
function UserProfile({ userId }) {
// 初期化用の「関数」を渡す。この関数は初回マウント時の一度だけ実行される!
const [userData, setUserData] = useState(() => {
console.log(“重い初期化処理が走りました(初回のみ)”);
return heavyCalculation(userId);
});
return
;
}
引数なしの関数(アロー関数など)を `useState` に渡すと、Reactはその関数を初回マウント時の一度だけ実行し、その戻り値を初期状態として採用する。再レンダリング時にはこの関数は完全に無視される。この小さな違いが、大規模なアプリケーションのパフォーマンスをじわじわと支えていくんだ。
—
3. 実務でそのまま使える!クリーンなサンプルコード
百聞は一見に如かず。ここまで解説した「配列の分割代入による綺麗な宣言」と「遅延初期化」を組み合わせた、実務レベルのコンポーネントを置いておく。
ローカルストレージから設定を読み込む、よくあるダッシュボードのウィジェットを想定してくれ。
import React, { useState } from ‘react’;
// 【模擬】ローカルストレージから重い設定データを読み込む初期化関数
const loadInitialSettings = () => {
console.log(‘— ローカルストレージへのアクセス(重い処理) —‘);
const savedSettings = localStorage.getItem(‘app_settings’);
if (savedSettings) {
try {
return JSON.parse(savedSettings);
} catch (e) {
console.error(‘JSONのパースに失敗しました’, e);
}
}
// デフォルト値
return { theme: ‘light’, notifications: true };
};
export const SettingsWidget = () => {
// 1. 配列の分割代入で、状態変数と更新関数をわかりやすく命名
// 2. 引数に「関数」を渡すことで、初期化コストを初回マウント時のみに限定(遅延初期化)
const [settings, setSettings] = useState(() => loadInitialSettings());
const [inputCount, setInputCount] = useState(0); // ついでに普通の数値ステート
// テーマを切り替えるハンドラー
const toggleTheme = () => {
setSettings((prevSettings) => {
const newSettings = {
…prevSettings,
theme: prevSettings.theme === ‘light’ ? ‘dark’ : ‘light’,
};
// ついでにストレージにも保存
localStorage.setItem(‘app_settings’, JSON.stringify(newSettings));
return newSettings;
});
};
return (
ユーザー設定パネル
現在のテーマ: {settings.theme}
{/ 再レンダリングの挙動を確認するためのカウンター /}
再レンダリング確認用カウンター: {inputCount}
);
};
—
シニアからのメッセージ
現場でコードを書いていると、「動けばいいや」で `useState(JSON.parse(localStorage.getItem(…)))` のようなコードを書いてしまいがちだ。しかし、そういった細部へのこだわりを積み重ねることが、のちに「なんかこのアプリ、妙にキビキビ動いて気持ちいいぞ」というユーザー体験の差になって返ってくる。
`useState` はただの箱じゃない。Reactという強力なライブラリが裏側でメモリを管理し、適切なタイミングでUIを同期させるための「神聖なインターフェース」だ。
仕組みの本質を理解し、一歩進んだ美しいコードを書いていこう。
さて、次は「非同期バグ対策(状態更新のキューイングと関数型アップデート)」について深掘りする予定だ。心してついてきてくれよ!

コメント