こんにちは。チームのコードレビューをしていて、最近一番「あぁ、またやってるな…」と頭を抱えたくなるアンチパターンが何だか分かるかい?そう、「派生状態(Derived State)の不要な`useState`による二重管理」だ。
中級に差し掛かったエンジニアがよくやりがちなんだよね。「データが変わったらこの変数も更新しなきゃ!」と意気込んで、`useEffect`やイベントハンドラの中でご丁寧に`setAnotherState`を呼んでしまう。
結果どうなるか? 状態の同期ズレ、無駄な再レンダリング、そして「なぜかUIと内部データが一致しない」という魔のバグの温床の出来上がりだ。
今日は、Reactの哲学の根幹に関わるこの「派生状態の計算」について、ブラウザの裏側の動きも含めて徹底的に叩き込んでいく。これをマスターするだけで、君の書くコードから無駄なステートが驚くほど消え、バグの確率を劇的に減らせるはずだ。さあ、いこうか。
—
なぜ「派生状態をstateに入れる」のは悪手なのか?
まずは、現場でよく見る「やってはいけない実装」を思い浮かべてほしい。例えば、ユーザーの「名(firstName)」と「姓(lastName)」という2つのstateがあって、そのフルネームを表示したいとする。
ここで、「フルネームという状態(fullName)」をわざわざ`useState`で持たせてしまう人がいる。
// ❌ やりがちなアンチパターン:派生状態をstateで管理している
const [firstName, setFirstName] = useState(‘太郎’);
const [lastName, setStateLastName] = useState(‘山田’);
const [fullName, setFullName] = useState(‘山田 太郎’); // ← これが不要!
// 姓や名が変わるたびに、手動で同期させようとする苦行
const handleFirstNameChange = (e) => {
setFirstName(e.target.value);
setFullName(`${e.target.value} ${lastName}`); // 忘れやすいしバグの元
};
これの何が問題か? 「真実のソース(Source of Truth)が複数存在してしまう」 ことだ。
`firstName`と`lastName`が決まれば、`fullName`は一意に計算できる(派生する)はずだよね。それなのにわざわざ別のstateとして保持するから、「名前は変わったのにフルネームの更新漏れが起きた」という致命的な同期ズレを生む。
Reactのレンダリングモデルとブラウザの裏側
ここで、Reactがブラウザ上でどう動いているかを思い出そう。
Reactは、状態(State)やプロパティ(Props)が変化すると、コンポーネント関数を再度実行(レンダリング)し、その戻り値である仮想DOMのツリーと前回のツリーを比較(差分検出:Reconciliation)して、DOMを更新する。
コンポーネントの関数が呼び出されるということは、関数内のローカル変数は毎回のレンダリングのたびに頭から再評価されるということだ。
つまり、`firstName`や`lastName`から計算できる値は、わざわざ`useState`の箱に入れて記憶させておく必要などない。レンダリングのたびに、その場で「ただのJavaScriptの変数として計算すればいい」のだ。メモリも食わないし、同期ズレも物理的に起こり得ない。最高だろう?
—
実務で使える:綺麗なコードとリファクタリングの実例
百聞は一見にしかず。ECサイトのショッピングカートを想定した、実務レベルのコンポーネントを見てみよう。
「商品ごとの数量(quantities)」と「各商品の単価」から、「カート内の合計金額」を計算するシーンだ。
悪い例:useState と useEffect で無理やり同期させている場合
import React, { useState, useEffect } from ‘react’;
// 商品データの型定義
type CartItem = {
id: string;
name: string;
price: number;
quantity: number;
};
const BadCart = ({ initialItems }: { initialItems: CartItem[] }) => {
const [items, setItems] = useState
// ❌ 状態から計算できるのに、わざわざstateに持たせている
const [totalPrice, setTotalPrice] = useState
// ❌ itemsが変わるたびにuseEffectで同期を取る(アンチパターンのコンボ)
useEffect(() => {
const calculated = items.reduce((sum, item) => sum + item.price item.quantity, 0);
setTotalPrice(calculated);
}, [items]);
const handleQuantityChange = (id: string, newQuantity: number) => {
setItems(prev => prev.map(item => item.id === id ? { …item, quantity: newQuantity } : item));
};
return (
合計金額: {totalPrice}円
);
};
このコードの何がダサい(そして危険な)か分かるかい?
1. `items`が更新される
2. レンダリングが走る
3. `useEffect`が発火する
4. `setTotalPrice`が呼ばれ、もう一度レンダリングが走る(無駄な再レンダリング)
Reactにわざわざ2回仕事をさせている上に、コードが冗長で読みづらい。これをシニアの腕の見せ所として、スッキリ書き換えよう。
良い例:レンダリング中に直接計算する(Derived Stateの活用)
import React, { useState, useMemo } from ‘react’;
type CartItem = {
id: string;
name: string;
price: number;
quantity: number;
};
const GoodCart = ({ initialItems }: { initialItems: CartItem[] }) => {
const [items, setItems] = useState
// ⭕ useStateもuseEffectも不要!レンダリング中にその場で計算する
// ※ 配列の要素数が膨大で、かつパフォーマンス上のボトルネックになる場合のみ useMemo を検討する
const totalPrice = items.reduce((sum, item) => sum + item.price item.quantity, 0);
const handleQuantityChange = (id: string, newQuantity: number) => {
setItems(prev => prev.map(item =>
item.id === id ? { …item, quantity: newQuantity } : item
));
// ここで totalPrice の更新を気にする必要は一切ない!
};
return (
handleQuantityChange(item.id, Number(e.target.value))}
/>
))}
{/ 常に最新の items から導出された正確な値が表示される /}
合計金額: {totalPrice.toLocaleString()}円
);
};
どうだろう? `useEffect`がごっそり消え、コードの見通しが劇的に良くなったよね。`items`が更新されれば、次のレンダリング時には`totalPrice`は自動的に最新の計算結果を保持して画面に描画される。これがReactの正しい姿だ。
—
シニアが教える例外と注意点:いつ `useMemo` を使うべきか?
「じゃあ、すべての計算を変数で持たせていいのか? 重い処理はどうするんだ」という鋭いツッコミが聞こえてきそうだね。その通り。例外はある。
JavaScriptの計算と言えど、数万件のデータをフィルタリングしたり、複雑なアルゴリズムを回したりする場合、毎回のレンダリングでその計算を走らせるのはメインスレッドをブロックし、UIのフリーズ(カクつき)に繋がる。
そういう時こそ、`useState`ではなく `useMemo` の出番だ。
// 計算コストが非常に高いデータフィルタリングの例
const filteredItems = useMemo(() => {
console.log(‘重い計算を実行中…’);
return items.filter(item => expensiveFilterLogic(item, searchQuery));
}, [items, searchQuery]); // itemsかsearchQueryが変わった時だけ再計算する
ここで重要なのは、`useMemo` はあくまで「パフォーマンス最適化のためのキャッシュフック」であって、状態管理の道具ではないということ。真実のソースはあくまで元の `items` や `searchQuery` であり、そこから派生した値であるという本質は変わらない。
—
まとめ:今日から意識すべきこと
派生状態の設計で迷ったら、自分にこう問いかけてみてほしい。
> 「この値は、他のStateやPropsから計算で導き出せるものじゃないか?」
もし答えが「Yes」なら、迷わず`useState`を削除し、コンポーネント内で直接計算するか、必要に応じて`useMemo`でメモ化しよう。それだけで、君の書くReactコードは驚くほど軽くなり、同期ズレのバグとは永遠にオサラバできるはずだ。
現場からは以上だ。次のコードレビューを楽しみにしているよ。

コメント