【テクニカル・上級編】 派生状態(Derived State)の計算と不要なuseStateの削減 – React実践ガイド

派生状態(Derived State)というアンチパターン:なぜシニアは `useState` を削るのか

こんにちは、あるいはこんばんは。日々、ReactのレンダリングパイプラインとV8エンジンのメモリ使用量に思いを馳せるフロントエンド・アーキテクチャの住人です。

コードレビューをしていて、もっとも「あぁ、またか」と頭を抱えたくなる瞬間、それは何だと思いますか?
複雑なデータフェッチの競合? `useEffect` の依存配列の漏れ? もちろんそれらも香ばしいですが、最上位に君臨するのは「既存の状態から計算できるはずの値なのに、わざわざ独立した `useState` で管理し、ご丁寧かつナイーブに `useEffect` で同期をとろうとしているコード」を見たときです。

今回は、この「派生状態(Derived State)」の罠と、そこから脱却して堅牢で高速なReactアプリケーションを構築するためのアーキテクチャ設計について、ブラウザの内部挙動やReactの Reconciliation(再調停)の哲学を交えながら、徹底的に深掘りしていきましょう。

—

1. なぜ「同期ズレ」は起きるのか? —— 二重管理の呪い

まずは、現場でよく見かける「やってはいけない実装」の典型例を見てください。ユーザーのファーストネームとラストネームから、フルネームを算出するコンポーネントです。

import React, { useState, useEffect } from ‘react’;

// 【アンチパターン】派生状態をuseStateで管理し、useEffectで同期しようとする例
const BadUserProfile: React.FC = () => {
const [firstName, setFirstName] = useState(‘John’);
const [lastName, setLastName] = useState(‘Doe’);

// 良くない設計:計算可能な値なのに独立したstateとして持っている
const [fullName, setFullName] = useState(‘John Doe’);

// firstNameやlastNameが変わるたびに、fullNameを同期させるという名の「二重管理」
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

return (

setFirstName(e.target.value)}
placeholder=”First Name”
/>
setLastName(e.target.value)}
placeholder=”Last Name”
/>

FullName: {fullName}

);
};

このコードの何がそんなにヤバいのでしょうか?

1. レンダリングの無駄打ち(Waterfall renders): ユーザーが入力するたびに、まず `firstName` の更新で1回目のレンダリングが発生し、その直後に `useEffect` が発火して `setFullName` が呼ばれ、2回目のレンダリングが強制される。
2. 一時的な不整合(Stale State / Flicker): 状態が非同期に更新される過程で、コンポーネントのライフサイクルやReactのバッチ処理のタイミングによっては、UI上で値が一時的にズレるバグの温床になる。
3. 認知負荷の増大: 「この `fullName` は本当に信頼できるデータなのか? どこかで書き換えられていないか?」という余計な疑念をコードリーディング時に生む。

Reactの公式ドキュメントでも「If you can calculate it, don’t state it(計算できるならstateにするな)」と強く戒められています。状態(State)とは、「時間が経っても変化し、かつ他の値から算出できない最小限のデータセット」でなければならないのです。

—

2. 解決策:レンダリング中に直接計算する(Pure Render)

答えは極めてシンプルです。状態を削り、レンダリングのたびにその場で(on-the-flyで)計算すればいいのです。Reactコンポーネントは、本質的に「現在の `props` と `state` を受け取り、UIの記述を返す純粋関数(Pure Function)」なのですから。

先ほどのコードを、シニアエンジニアの美学に則ってリファクタリングしてみましょう。

import React, { useState } from ‘react’;

// 【正しい設計】計算可能な値は、レンダリング中に直接導出する
const GoodUserProfile: React.FC = () => {
const [firstName, setFirstName] = useState(‘John’);
const [lastName, setLastName] = useState(‘Doe’);

// 派生状態(Derived State)の基本:ただのローカル変数として計算する
// 余計なuseStateもuseEffectも存在しない。
const fullName = `${firstName} ${lastName}`;

return (

setFirstName(e.target.value)}
placeholder=”First Name”
/>
setLastName(e.target.value)}
placeholder=”Last Name”
/>

FullName: {fullName}

);
};

これだけで、`useEffect` は消え去り、レンダリングパスは1回に削減され、状態の同期ズレる余地は完全に消失しました。コードの行数も減り、メンテナビリティは劇的に向上します。これが、余分な `useState` を削る最大のメリットです。

—

3. 「計算コストが高い場合」はどうする? —— `useMemo` の正しい処方箋

「いやいや、うちのアプリケーションで計算しようとしている派生データは、数万件のオブジェクトを走査する重い処理なんだよ! レンダリングのたびに計算したらメインスレッドがブロックされてブラウザがカクつく!」

そんな鋭いツッコミが飛んでくる頃合いですね。その通り、すべての計算を毎回素通しで行うのが正解とは限りません。ここで登場するのが `useMemo` フックです。

ただし、「重い処理だからとりあえず `useMemo` を使う」という思考停止は禁物です。`useMemo` 自体もメモリ(前回の依存配列と結果を保持するためのキャッシュ領域)を消費し、依存配列のシャローコピー比較(Object.is)のオーバーヘッドを伴います。

本当にメモ化が必要な境界線を見極めた、実務レベルのコードを見てみましょう。

import React, { useState, useMemo } from ‘react’;

interface Transaction {
id: string;
amount: number;
category: string;
}

const ExpensiveDashboard: React.FC<{ transactions: Transaction[] }> = ({ transactions }) => {
const [filterCategory, setFilterCategory] = useState(‘all’);
const [searchTerm, setSearchTerm] = useState(”);

// 【計算コストが高い派生データの導出】
// transactions配列が数万件規模、かつフィルタリングや集計処理が重い場合のみuseMemoを使う。
const filteredAndTotalAmount = useMemo(() => {
console.log(‘重い計算処理を実行中…’); // パフォーマンス測定用のログ

const filtered = transactions.filter(tx => {
const matchesCategory = filterCategory === ‘all’ || tx.category === filterCategory;
const matchesSearch = tx.id.includes(searchTerm);
return matchesCategory && matchesSearch;
});

const total = filtered.reduce((sum, tx) => sum + tx.amount, 0);

return { filtered, total };
}, [transactions, filterCategory, searchTerm]); // 依存する値が変わった時だけ再計算

return (

{/ フィルターや検索のUI /}

合計金額: {filteredAndTotalAmount.total} 円

    {filteredAndTotalAmount.filtered.map(tx => (

  • {tx.category}: {tx.amount}円
  • ))}

);
};

ここで重要なアーキテクチャの知見として、「プリミティブな値の単純な結合や、数個の配列の `.map()` 程度で `useMemo` を使ってはいけない」というルールがあります。V8エンジンのJITコンパイラにとって、数個のオブジェクトの生成や単純な文字列結合など朝飯前です。むしろ、`useMemo` のフック構造体を維持するコストの方が高くなるケースすらあります。

プロファイラ(React DevTools)で実際にパフォーマンスボトルネックが観測されたときのみ、`useMemo` というメスを入れる。これがプロフェッショナルのアプローチです。

—

4. プロップスを `useState` の初期値にコピーするアンチパターン

派生状態の議論で最も深刻なバグを生むのが、「親から受け取ったプロップスを、そのまま子コンポーネントの `useState` の初期値として代入してしまうケース」です。

// 【極めて危険なアンチパターン】
const DangerChild: React.FC<{ initialUser: { name: string } }> = ({ initialUser }) => {
// プロップスをstateの初期値にしている
// 親の initialUser.name が変わっても、このstateは「初期化時の一度きり」しか更新されない!
const [name, setName] = useState(initialUser.name);

return setName(e.target.value)} />;
};

この実装を行うと、親コンポーネント側で `initialUser` のデータを再フェッチして新しい名前に更新したとしても、子コンポーネント内の `name` は古い初期値のまま固着し、親子のデータが完全に乖離するという泥沼のバグ(同期不全)が発生します。

もし「親から受け取ったデータを基にしつつ、子コンポーネント側で独自の編集状態を持ち、かつ親の変更にも追従させたい(あるいは完全にデータを同期させたい)」場合は、以下のいずれかの設計を採用すべきです。

1. 制御されたコンポーネント(Controlled Component)にする: 子は独自のstateを持たず、すべて親のstateとコールバックに委譲する。
2. Key属性によるコンポーネントの強制リセット(Uncontrolled with Key): 親のデータが切り替わるタイミングで、子コンポーネントに `key={user.id}` のように一意なキーを付与し、propsの変更時にReactにDOMツリーごと古いインスタンスを破棄・再生成させる。

// 解決策1: Keyを使って親の変更時にコンポーネントを丸ごとリフレッシュする
const Parent: React.FC = () => {
const [user, setUser] = useState({ id: ‘1’, name: ‘Alice’ });

return (

);
};

const SafeChild: React.FC<{ user: { id: string; name: string } }> = ({ user }) => {
// keyが変わるため、このuseStateは常に最新のプロップスで初期化されることが保証される
const [name, setName] = useState(user.name);

return setName(e.target.value)} />;
};

—

5. まとめ:状態管理のミニマリズムを極める

Reactアプリケーションのアーキテクチャが美しく、バグが少なく、かつハイパフォーマンスである秘訣は、突き詰めると「どれだけ状態(State)を削ぎ落とせるか」というミニマリズムに集約されます。

  • 「計算できるものは、状態にしない」(常にその場で算出する)
  • 「プロップスをそのまま `useState` の初期値にコピーしない」(同期不全の元凶を断つ)
  • 「メモ化は計測結果に基づいて慎重に行う」(無駄な最適化は悪)

この原則をチーム全体で徹底するだけで、コードベースから数百行の冗長な `useEffect` やバグだらけの同期ロジックが消え去り、驚くほどスケーラブルで堅牢なWebアプリケーションへと生まれ変わります。

さあ、あなたのエディタを開いて、無駄な `useState` をすっきりとリファクタリングしに行きましょう。コードが軽くなる快感を、ぜひその手で味わってみてください。

コメント

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