React開発における状態管理のスケーラブルな構造について考えてみる

React開発における状態管理のスケーラブルな構造とは 「拡張性があり、保守しやすく、チームやプロダクトの成長に対応できる構成」のとして考えます。
小さなアプリケーションでは useState
や useContext
で事足りることもありますが、機能が増え、開発メンバーが増えてくると、それでは破綻してしまいます。
本記事では、Reactアプリケーションにおける状態管理のスケーラブルな構造について、実際の構成例やコードを交えながら徹底解説していきます。
状態管理に求められるスケーラビリティとは
状態管理がスケーラブルであるためには、以下のような条件を満たす必要があります。
要件 | 説明 |
---|---|
分離性 | 状態とUI、ロジックが適切に分離されている |
再利用性 | コンポーネントや状態が再利用可能な形で設計されている |
拡張性 | 新しい機能を追加しても構造が崩れにくい |
テスト容易性 | 単体テストが書きやすく保守性が高い |
型安全性 | TypeScriptなどを用いて安全な状態管理ができる |
これらを満たすことで、数人のプロジェクトから数十人規模までスムーズにスケール可能になります。
小規模構成での状態管理の限界
まずは小さな構成から出発し、どのようにスケーラブルな構成へと変化していくのかを見ていきます。
useStateとuseContextだけの構成
tsx// App.tsx
import React, { createContext, useContext, useState } from 'react';
const UserContext = createContext(null);
export default function App() {
const [user, setUser] = useState({ name: 'Taro', isLoggedIn: true });
return (
<UserContext.Provider value={{ user, setUser }}>
<Header />
<Dashboard />
</UserContext.Provider>
);
}
function Header() {
const { user } = useContext(UserContext);
return <div>Hello, {user.name}</div>;
}
この構成は単純ですが、以下の問題があります。
- コンテキストが1ファイルに集中してしまい、責務が曖昧になる
- 状態が増えるとProviderツリーが深くなり、管理しにくくなる
- コンポーネントの再利用性が落ちる(特定のコンテキストに依存)
カスタムフックとドメインごとの分離
ここから改善を加えていきましょう。
状態を機能ごとに分離する
tsx// features/user/hooks/useUser.ts
import { useState } from 'react';
export const useUser = () => {
const [user, setUser] = useState({ name: 'Taro', isLoggedIn: false });
const login = (name: string) => setUser({ name, isLoggedIn: true });
const logout = () => setUser({ name: '', isLoggedIn: false });
return { user, login, logout };
};
呼び出し側で使う
tsx// App.tsx
import { useUser } from './features/user/hooks/useUser';
export default function App() {
const { user, login, logout } = useUser();
return (
<div>
<h1>Hello, {user.name}</h1>
<button onClick={() => login('Jiro')}>Login</button>
<button onClick={logout}>Logout</button>
</div>
);
}
この構成では、状態のロジックをUIから完全に分離できます。
グローバルステート管理への移行:Zustandの例
中〜大規模アプリではグローバルな状態管理が必要になります。
Zustand
は軽量かつシンプルでありながら、拡張性が高いためスケーラブルな構成にも適しています。
Zustandでの構成例
tsx// stores/userStore.ts
import { create } from 'zustand';
type User = {
name: string;
isLoggedIn: boolean;
};
type State = {
user: User;
login: (name: string) => void;
logout: () => void;
};
export const useUserStore = create<State>((set) => ({
user: { name: '', isLoggedIn: false },
login: (name) => set({ user: { name, isLoggedIn: true } }),
logout: () => set({ user: { name: '', isLoggedIn: false } }),
}));
tsx// components/UserPanel.tsx
import { useUserStore } from '../stores/userStore';
export const UserPanel = () => {
const { user, login, logout } = useUserStore();
return (
<div>
<p>Welcome, {user.name}</p>
<button onClick={() => login('Hanako')}>Login</button>
<button onClick={logout}>Logout</button>
</div>
);
};
Zustandを用いることで、依存性の注入を減らし、各コンポーネントが自立的に状態にアクセス可能になります。
スケーラブルな構成のパターン例
以下は状態管理を含む構成ディレクトリの一例です。
csssrc/
├── app/
│ └── providers.tsx
├── features/
│ ├── user/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── stores/
│ │ └── types/
│ ├── post/
│ └── ...
├── shared/
│ ├── components/
│ ├── hooks/
│ └── utils/
特徴としては以下の通りです。
特徴 | 説明 |
---|---|
ドメイン単位で機能を分離 | features/user , features/post などに細分化 |
状態とロジックを切り出す | stores , hooks を通じてドメインごとに責務を明確化 |
共通要素はsharedへ集約 | ボタンやフォームなど共通UIは shared/components に配置 |
状態管理のテスト戦略
スケーラブルな構成では、ユニットテストのしやすさも重要です。
Zustandのstoreをテストする例
tsimport { useUserStore } from './userStore';
describe('User Store', () => {
it('login updates state', () => {
const store = useUserStore.getState();
store.login('Taro');
expect(store.user.name).toBe('Taro');
expect(store.user.isLoggedIn).toBe(true);
});
it('logout resets state', () => {
const store = useUserStore.getState();
store.logout();
expect(store.user.name).toBe('');
expect(store.user.isLoggedIn).toBe(false);
});
});
Zustandでは getState()
を用いて直接状態を取得・操作できるため、テストコードもシンプルに書けます。
他の選択肢との比較(Recoil, Jotai, Redux)
ライブラリ | 特徴 | 適したケース |
---|---|---|
Zustand | 軽量・非依存・簡潔 | 機能分離された中〜大規模構成 |
Recoil | アトミックな設計が可能 | 複雑な依存関係を持つ構成 |
Jotai | React標準に近く学習コストが低い | 小〜中規模アプリ |
Redux Toolkit | 明示的で堅牢、エコシステムが豊富 | チーム開発・大規模 |
まとめ:スケーラブルなReact状態管理の最適解
Reactアプリケーションの成長に対応するには、状態管理の構成も進化させる必要があります。
本記事で紹介したように、次のステップを踏むことが重要です。
useState
からuseContext
、カスタムフックへ- 状態をドメイン単位で分離し再利用性を高める
Zustand
やRecoil
などの軽量な状態管理ライブラリでグローバルステートを導入- テストや型安全性を担保し、チームでも扱いやすくする
- フォルダ構成・責務分離により拡張性を確保する
こうした構成を取ることで、機能追加やリファクタリングがスムーズになり、チーム開発も格段に進めやすくなります。
ぜひ、ご自身のプロジェクトに合ったスケーラブルな状態管理構成を取り入れてみてください。
記事Article
もっと見る- article
Reactの状態管理2025:「useState」「Redux Toolkit」「Jotai」「Zustand」を比較してみた
- article
Next.jsでの画像最適化戦略:next/image vs 外部CDNを比較してみた
- article
React Server Componentsの可能性と課題を実用に向けて考えてみる
- article
Next.js 13 App Router入門:Pages Routerとの違いと移行のコツをわかりやすく紹介
- article
ReactとSuspenseで構築する柔軟な非同期UIの設計法について紹介
- article
Next.js開発でちょいちょい発生する、ハイドレーションエラーの原因と対策を紹介