概要
状態管理とは「値を保存する」ことではない。
それは**「UIとデータを最小コストで同期させ続ける構造設計」**である。
状態が肥大化すればロジックは壊れ、
共有しすぎればコンポーネントは鈍重になり、
非正規化された構造はバグの温床となる。
本稿では、以下の観点からモダンJavaScriptアプリケーションにおける状態管理設計戦略を提示する:
- ローカル vs グローバルの責務定義
- 再レンダリング最小化の構造
- ステートの正規化と派生設計
- 非同期状態と一貫性の保証
- 永続化・ストレージとの橋渡し
1. ローカル vs グローバル状態の原則
✅ ローカル状態(UIスコープ)
- ボタンの押下状態
- モーダルの開閉
- 現在のページ番号
const [isOpen, setIsOpen] = useState(false);
✅ グローバル状態(アプリ全体に影響)
- ログインユーザー情報
- カート内容
- アプリ設定
import { userStore } from '@/stores/user';
→ ✅ **「誰が管理し、誰が使うか」**で切り分けるのが本質
2. ステート正規化:配列をマップで持つ構造
// ❌ 非正規化
const users = [
{ id: 'u01', name: 'Toto' },
{ id: 'u02', name: 'Mir' },
];
// ✅ 正規化
const userById = {
u01: { id: 'u01', name: 'Toto' },
u02: { id: 'u02', name: 'Mir' },
};
const userIds = ['u01', 'u02'];
- ✅ 再取得・更新がO(1)
- ✅ 重複を回避し、一貫性を保てる
- ✅ Redux Toolkitなどの正規化支援ライブラリも有効
3. 派生状態(Derived State)の戦略
const totalPrice = cartItems.reduce((sum, item) => sum + item.price, 0);
- ✅ 派生値は状態に含めない
- ✅ 関数で導出することで信頼性を担保
- ✅ memo化・selector設計が重要(Recoil, Zustand など)
4. 再レンダリングを最小化する構造
// ❌ すべての状態を1つにまとめると全部が再描画
const [state, setState] = useState({ a, b, c });
// ✅ 状態を粒度ごとに分割
const [count, setCount] = useState(0);
const [input, setInput] = useState('');
- ✅ 粒度を分けることでパフォーマンスが安定
- ✅ グローバルステートでも購読単位を最小に
5. 状態と永続化ストレージの接続設計
useEffect(() => {
const stored = localStorage.getItem('theme');
if (stored) setTheme(stored);
}, []);
- ✅ ステートとストレージを直接結ばず、Adapterを設計
- ✅ JSON.parse / stringify の一元化
- ✅ SSR対応時はhydrationタイミングの制御が必要
設計判断フロー
① この状態はどこで必要? → ローカル/グローバル切り分け
② 同一データを複数場所で参照していないか? → 正規化へ
③ 状態に「結果」ではなく「原因」を保存しているか? → 派生は関数に分離
④ 状態が肥大化していないか? → 粒度分割 + 再レンダリング最適化
⑤ ストレージと直結していないか? → Adapter or hook経由に構造化
よくあるミスと対策
❌ すべての状態をグローバルに持たせてUIが鈍重化
→ ✅ **“必要なところにだけ届ける”**という設計の逆算を行う
❌ データの冗長表現が複数存在し、片方だけ更新されバグに
→ ✅ 正規化 + セレクターによる導出に切り替える
❌ 状態が深くネストされて操作が難解に
→ ✅ 構造をフラットに、参照・更新の経路を明示的に
結語
状態管理とは「データを保管する」ことではない。
それはUIの整合性とアプリ全体の一貫性を保証する、構造の中核設計である。
- スコープで役割を分け
- 粒度で責任を分離し
- 計算と保存を区別する
JavaScriptにおける状態管理設計とは、
“壊れず、重くならず、説明可能な構造をつくる”ための戦略である。