- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 1. 2025年(ねん)の状態管理(じょうたいかんり)ランドスケープ
- 2. サーバー状態革命(かくめい):TanStack QueryとSWR
- 3. React Context:いつ十分(じゅうぶん)か
- 4. Redux Toolkit:まだ必要(ひつよう)か?
- 5. Zustand:ミニマリズムの勝利(しょうり)
- 6. Jotai:原子的状態(げんしてきじょうたい)の力(ちから)
- 7. Recoil:Metaの選択(せんたく)、しかし衰退(すいたい)
- 8. Signals:新(あたら)しいパラダイム
- 9. 総合比較表(そうごうひかくひょう)
- 10. プロジェクト別(べつ)選択(せんたく)ガイド
- 11. マイグレーションガイド
- 12. 面接質問(めんせつしつもん)とクイズ
- 参考資料(さんこうしりょう)
1. 2025年(ねん)の状態管理(じょうたいかんり)ランドスケープ
npmダウンロードトレンド
2025年(ねん)のReactエコシステムにおける状態管理(じょうたいかんり)ライブラリの競争(きょうそう)はかつてないほど激(はげ)しくなっています。週間(しゅうかん)ダウンロード数(すう)の内訳(うちわけ)は以下(いか)の通(とお)りです。
| ライブラリ | 週間(しゅうかん)ダウンロード(2025) | バンドルサイズ | 初回(しょかい)リリース |
|---|---|---|---|
| Redux (+ Toolkit) | ~9M | 11KB (RTK) | 2015 |
| Zustand | ~5.5M | 1.1KB | 2019 |
| TanStack Query | ~4.5M | 13KB | 2020 |
| Jotai | ~2.2M | 3.4KB | 2020 |
| Recoil | ~500K | 21KB | 2020 |
パラダイムの転換(てんかん)
2025年(ねん)の最(もっと)も重要(じゅうよう)な変化(へんか)は**サーバー状態(じょうたい)とクライアント状態(じょうたい)の分離(ぶんり)**です。以前(いぜん)はReduxで全(すべ)てを管理(かんり)していましたが、今(いま)は明確(めいかく)な役割分担(やくわりぶんたん)が行(おこな)われています。
- サーバー状態(じょうたい): TanStack Query / SWR(APIデータ、キャッシング、同期(どうき))
- クライアント状態(じょうたい): Zustand / Jotai / Redux Toolkit(UI状態(じょうたい)、フォーム状態(じょうたい)、テーマ)
- URL状態(じょうたい): nuqs / next-usequerystate(検索(けんさく)フィルタ、ページネーション)
- フォーム状態(じょうたい): React Hook Form / Formik(入力値(にゅうりょくち)、バリデーション)
┌─────────────────────────────────────────────────┐
│ Application State │
├──────────────┬──────────────┬───────────────────┤
│ Server State │ Client State │ URL / Form State │
│ TanStack Q │ Zustand │ nuqs + RHF │
│ SWR │ Jotai │ │
│ │ Redux TK │ │
└──────────────┴──────────────┴───────────────────┘
2. サーバー状態革命(かくめい):TanStack QueryとSWR
なぜサーバー状態(じょうたい)を分離(ぶんり)するのか
従来(じゅうらい)のReduxパターンでは、API呼(よ)び出(だ)しの結果(けっか)をグローバルストアに保存(ほぞん)していました。しかし、このアプローチには深刻(しんこく)な問題(もんだい)があります。
- キャッシュ無効化(むこうか): データを再取得(さいしゅとく)するタイミングを手動(しゅどう)で管理(かんり)する必要(ひつよう)がある
- ローディング/エラー状態(じょうたい): 毎回(まいかい)isLoadingとerror状態(じょうたい)を手動(しゅどう)で管理(かんり)
- 重複(ちょうふく)リクエスト: 複数(ふくすう)のコンポーネントが同(おな)じデータを要求(ようきゅう)すると重複(ちょうふく)API呼(よ)び出(だ)しが発生(はっせい)
- 楽観的更新(らっかんてきこうしん): 実装(じっそう)が非常(ひじょう)に複雑(ふくざつ)
TanStack Query (React Query v5)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// サーバー状態の取得 - キャッシングと再検証を自動処理
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json()),
staleTime: 5 * 60 * 1000, // 5分間freshを維持
gcTime: 30 * 60 * 1000, // 30分後にガベージコレクション
})
}
// サーバー状態の変更 - 楽観的更新を含む
function useCreateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (newUser: User) =>
fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser),
}).then(res => res.json()),
// 楽観的更新
onMutate: async (newUser) => {
await queryClient.cancelQueries({ queryKey: ['users'] })
const previous = queryClient.getQueryData(['users'])
queryClient.setQueryData(['users'], (old: User[]) => [...old, newUser])
return { previous }
},
onError: (err, newUser, context) => {
queryClient.setQueryData(['users'], context?.previous)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
}
Stale-While-Revalidateパターン
ユーザーリクエスト → キャッシュ確認 → キャッシュデータを即座に返却 (stale)
↓
バックグラウンドで新データを取得 (revalidate)
↓
新データでUIを更新
このパターンのおかげで、ユーザーは常(つね)に即座(そくざ)の応答(おうとう)を受(う)けながら、最新(さいしん)データを自動的(じどうてき)に受(う)け取(と)ります。
SWR vs TanStack Query比較(ひかく)
| 機能(きのう) | TanStack Query v5 | SWR v2 |
|---|---|---|
| バンドルサイズ | 13KB | 4.2KB |
| Mutationサポート | useMutation内蔵(ないぞう) | 手動(しゅどう)実装(じっそう) |
| 楽観的更新(らっかんてきこうしん) | ファーストクラスサポート | 手動(しゅどう)実装(じっそう) |
| Devtools | 専用(せんよう)DevTools | なし |
| 無限(むげん)スクロール | useInfiniteQuery | useSWRInfinite |
| SSRサポート | Hydration API | Next.js統合(とうごう) |
3. React Context:いつ十分(じゅうぶん)か
Contextが適切(てきせつ)な場合(ばあい)
React Contextは追加(ついか)ライブラリなしで使用(しよう)できる組(く)み込(こ)みの状態共有(じょうたいきょうゆう)メカニズムです。
// テーマ、認証、ロケールなど変更が稀な状態に適合
const ThemeContext = createContext<ThemeContextType | null>(null)
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const value = useMemo(() => ({ theme, setTheme }), [theme])
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}
function useTheme() {
const context = useContext(ThemeContext)
if (!context) throw new Error('useTheme must be used within ThemeProvider')
return context
}
Contextのパフォーマンスの罠(わな)
Contextの最大(さいだい)の問題(もんだい)は、valueが変更(へんこう)されると全(すべ)ての消費者(しょうひしゃ)が再(さい)レンダリングされることです。
// 悪いパターン:すべての状態を1つのContextに
const AppContext = createContext({
user: null,
theme: 'light',
notifications: [],
sidebar: false,
})
// 問題:sidebarだけ変更しても、user、theme、notificationsを
// 使用する全てのコンポーネントが再レンダリングされます!
Compositionパターンで解決(かいけつ)
// 良いパターン:状態ごとにContextを分離
function App() {
return (
<ThemeProvider>
<AuthProvider>
<NotificationProvider>
<SidebarProvider>
<Layout />
</SidebarProvider>
</NotificationProvider>
</AuthProvider>
</ThemeProvider>
)
}
// 各Contextは自分の状態のみを管理するため
// 不要な再レンダリングが発生しません
Contextが不十分(ふじゅうぶん)な場合(ばあい)
以下(いか)の状況(じょうきょう)では専用(せんよう)の状態管理(じょうたいかんり)ライブラリが必要(ひつよう)です。
- 状態(じょうたい)が頻繁(ひんぱん)に変更(へんこう)される場合(ばあい)(カウンタ、タイマー、リアルタイムデータ)
- 特定(とくてい)の状態(じょうたい)スライスに対(たい)する細粒度(さいりゅうど)のサブスクリプションが必要(ひつよう)な場合(ばあい)
- ミドルウェアが必要(ひつよう)な場合(ばあい)(ロギング、永続化(えいぞくか)、開発者(かいはつしゃ)ツール)
- 状態変更(じょうたいへんこう)ロジックが複雑(ふくざつ)な場合(ばあい)
4. Redux Toolkit:まだ必要(ひつよう)か?
RTKの現在(げんざい)の位置(いち)
Redux Toolkit(RTK)はReduxのボイラープレート問題(もんだい)を解決(かいけつ)し、公式(こうしき)推奨(すいしょう)となりました。2025年(ねん)でも大規模(だいきぼ)エンタープライズで依然(いぜん)として強力(きょうりょく)な選択肢(せんたくし)です。
createSlice:ボイラープレートの排除(はいじょ)
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
interface TodoState {
items: Todo[]
filter: 'all' | 'active' | 'completed'
}
const todoSlice = createSlice({
name: 'todos',
initialState: { items: [], filter: 'all' } as TodoState,
reducers: {
addTodo: (state, action: PayloadAction<string>) => {
// Immerが内蔵されているため不変性を気にする必要がありません
state.items.push({
id: crypto.randomUUID(),
text: action.payload,
completed: false,
})
},
toggleTodo: (state, action: PayloadAction<string>) => {
const todo = state.items.find(t => t.id === action.payload)
if (todo) todo.completed = !todo.completed
},
setFilter: (state, action: PayloadAction<TodoState['filter']>) => {
state.filter = action.payload
},
},
})
export const { addTodo, toggleTodo, setFilter } = todoSlice.actions
export default todoSlice.reducer
RTK Query:組(く)み込(こ)みサーバー状態管理(じょうたいかんり)
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['User', 'Post'],
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
providesTags: ['User'],
}),
createUser: builder.mutation<User, Partial<User>>({
query: (body) => ({
url: '/users',
method: 'POST',
body,
}),
invalidatesTags: ['User'],
}),
}),
})
export const { useGetUsersQuery, useCreateUserMutation } = api
Redux Toolkitが依然(いぜん)として必要(ひつよう)な場合(ばあい)
- 大規模(だいきぼ)チーム: 厳格(げんかく)なパターンとコンベンションが必要(ひつよう)なチーム
- 複雑(ふくざつ)なビジネスロジック: 複数(ふくすう)の状態間(じょうたいかん)の複雑(ふくざつ)な相互作用(そうごさよう)
- タイムトラベルデバッグ: Redux DevToolsの強力(きょうりょく)なデバッグ
- ミドルウェアエコシステム: saga、thunkなど豊富(ほうふ)なミドルウェアサポート
- レガシー互換性(ごかんせい): 既存(きそん)のReduxコードベースからの段階的(だんかいてき)マイグレーション
5. Zustand:ミニマリズムの勝利(しょうり)
なぜZustandか
Zustandは1.1KBという驚異的(きょういてき)なバンドルサイズの最(もっと)も軽量(けいりょう)な状態管理(じょうたいかんり)ライブラリです。Providerが不要(ふよう)で、ボイラープレートが最小限(さいしょうげん)で、TypeScriptと完全(かんぜん)に互換性(ごかんせい)があります。
基本(きほん)ストアパターン
import { create } from 'zustand'
interface BearStore {
bears: number
increase: () => void
decrease: () => void
reset: () => void
}
const useBearStore = create<BearStore>((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
decrease: () => set((state) => ({ bears: state.bears - 1 })),
reset: () => set({ bears: 0 }),
}))
// 使用:Providerなしでどこからでも呼び出し
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <span>{bears} bears</span>
}
function BearControls() {
const increase = useBearStore((state) => state.increase)
return <button onClick={increase}>Add bear</button>
}
ミドルウェア:persist、devtools、immer
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
interface AppStore {
user: User | null
preferences: Preferences
setUser: (user: User | null) => void
updatePreference: (key: string, value: unknown) => void
}
const useAppStore = create<AppStore>()(
devtools(
persist(
immer((set) => ({
user: null,
preferences: { theme: 'light', language: 'ja' },
setUser: (user) => set((state) => {
state.user = user
}),
updatePreference: (key, value) => set((state) => {
state.preferences[key] = value
}),
})),
{
name: 'app-storage', // localStorageキー
partialize: (state) => ({
preferences: state.preferences,
}),
}
),
{ name: 'AppStore' } // DevToolsラベル
)
)
スライスパターン:大規模(だいきぼ)ストアの分割(ぶんかつ)
// userSlice.ts
const createUserSlice = (set: SetState, get: GetState) => ({
user: null,
login: async (credentials: Credentials) => {
const user = await authApi.login(credentials)
set({ user })
},
logout: () => set({ user: null }),
})
// cartSlice.ts
const createCartSlice = (set: SetState, get: GetState) => ({
items: [],
addItem: (item: CartItem) =>
set((state) => ({ items: [...state.items, item] })),
totalPrice: () =>
get().items.reduce((sum, item) => sum + item.price, 0),
})
// 統合
const useStore = create((...args) => ({
...createUserSlice(...args),
...createCartSlice(...args),
}))
Zustandの強(つよ)み
- 1.1KB バンドルサイズ (gzipped)
- Provider不要(ふよう): コンポーネントツリーのどこからでも使用可能(かのう)
- 選択的(せんたくてき)サブスクリプション:
(state) => state.bearsで必要(ひつよう)な状態(じょうたい)のみサブスクライブ - React外(そと)でも動作(どうさ): Vanilla JSでも使用可能(かのう)
- SSR互換(ごかん): Next.jsと完全(かんぜん)互換(ごかん)
6. Jotai:原子的状態(げんしてきじょうたい)の力(ちから)
Atomベースの設計(せっけい)
Jotaiは「原子(アトム)」単位(たんい)で状態(じょうたい)を管理(かんり)します。各(かく)atomは独立(どくりつ)しており、サブスクライブしているコンポーネントのみが再(さい)レンダリングされます。
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
// 基本atom
const countAtom = atom(0)
const nameAtom = atom('Guest')
// 派生atom (derived)
const doubleCountAtom = atom((get) => get(countAtom) * 2)
// 読み書きatom
const countWithLogAtom = atom(
(get) => get(countAtom),
(get, set, newValue: number) => {
console.log(`Count changed: ${get(countAtom)} -> ${newValue}`)
set(countAtom, newValue)
}
)
// 使用
function Counter() {
const [count, setCount] = useAtom(countAtom)
const doubleCount = useAtomValue(doubleCountAtom)
return (
<div>
<p>Count: {count}, Double: {doubleCount}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
)
}
非同期(ひどうき)AtomとSuspense統合(とうごう)
// 非同期atom - React Suspenseと自動統合
const userAtom = atom(async () => {
const response = await fetch('/api/user')
return response.json()
})
// 依存関係のある非同期atom
const userPostsAtom = atom(async (get) => {
const user = await get(userAtom)
const response = await fetch(`/api/users/${user.id}/posts`)
return response.json()
})
// Suspenseでローディング処理
function UserPosts() {
const posts = useAtomValue(userPostsAtom)
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserPosts />
</Suspense>
)
}
atomFamily:動的(どうてき)Atom生成(せいせい)
import { atomFamily } from 'jotai/utils'
// IDごとに独立したatomを生成
const todoAtomFamily = atomFamily((id: string) =>
atom({
id,
text: '',
completed: false,
})
)
// 各Todoは独立したatomで管理されるため
// 1つのTodoの変更が他のTodoの再レンダリングを引き起こしません
function TodoItem({ id }: { id: string }) {
const [todo, setTodo] = useAtom(todoAtomFamily(id))
return (
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => setTodo(prev => ({ ...prev, completed: !prev.completed }))}
/>
{todo.text}
</label>
)
}
Jotaiの強(つよ)み
- 3.4KB バンドルサイズ (gzipped)
- 原子的(げんしてき)再(さい)レンダリング: 変更(へんこう)されたatomをサブスクライブするコンポーネントのみ再(さい)レンダリング
- React Suspense統合(とうごう): 非同期状態(ひどうきじょうたい)を宣言的(せんげんてき)に処理(しょり)
- ボトムアップアプローチ: 小(ちい)さな単位(たんい)から状態(じょうたい)を構成(こうせい)
- DevToolsサポート: Jotai DevTools、React DevTools統合(とうごう)
7. Recoil:Metaの選択(せんたく)、しかし衰退(すいたい)
Recoilの現在(げんざい)の状態(じょうたい)
RecoilはMeta(Facebook)が作成(さくせい)した原子的状態管理(げんしてきじょうたいかんり)ライブラリです。しかし2025年(ねん)現在(げんざい)、メンテナンスが停滞(ていたい)し、リリース間隔(かんかく)が長(なが)くなっています。
基本的(きほんてき)な使用法(しようほう)
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil'
const todoListAtom = atom({
key: 'todoList',
default: [] as Todo[],
})
const filteredTodoSelector = selector({
key: 'filteredTodos',
get: ({ get }) => {
const list = get(todoListAtom)
const filter = get(todoFilterAtom)
switch (filter) {
case 'completed': return list.filter(t => t.completed)
case 'active': return list.filter(t => !t.completed)
default: return list
}
},
})
// atomFamily - 動的atom
const userAtomFamily = atomFamily({
key: 'user',
default: (userId: string) => async () => {
const res = await fetch(`/api/users/${userId}`)
return res.json()
},
})
Recoil vs Jotai
| 項目(こうもく) | Recoil | Jotai |
|---|---|---|
| バンドルサイズ | 21KB | 3.4KB |
| Key必要(ひつよう) | 必須(ひっす)(文字列(もじれつ)key) | 不要(ふよう) |
| Provider | RecoilRoot必須(ひっす) | 任意(にんい) |
| メンテナンス | 停滞(ていたい) | 活発(かっぱつ) |
| SSR | 制限的(せいげんてき) | 完全(かんぜん)サポート |
| コミュニティ | 減少傾向(げんしょうけいこう) | 増加傾向(ぞうかけいこう) |
JotaiがRecoilの全(すべ)ての機能(きのう)をより小(ちい)さなバンドルで提供(ていきょう)し、活発(かっぱつ)にメンテナンスされているため、新規(しんき)プロジェクトではJotaiを推奨(すいしょう)します。
8. Signals:新(あたら)しいパラダイム
Signalsとは
Signalsは細粒度(さいりゅうど)の反応性(はんのうせい)(fine-grained reactivity)を提供(ていきょう)する状態(じょうたい)プリミティブです。Reactの再(さい)レンダリングモデルとは異(こと)なり、Signalが変更(へんこう)されると、そのSignalを使用(しよう)するDOM部分(ぶぶん)のみが直接(ちょくせつ)更新(こうしん)されます。
Preact Signals
import { signal, computed, effect } from '@preact/signals-react'
// Signal作成
const count = signal(0)
const doubled = computed(() => count.value * 2)
// コンポーネントで使用 - 再レンダリングなしでDOM直接更新
function Counter() {
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => count.value++}>+1</button>
</div>
)
}
// 副作用
effect(() => {
console.log(`Count is now: ${count.value}`)
})
SolidJS Signals
import { createSignal, createMemo, createEffect } from 'solid-js'
function Counter() {
const [count, setCount] = createSignal(0)
const doubled = createMemo(() => count() * 2)
createEffect(() => {
console.log(`Count: ${count()}`)
})
return (
<div>
<p>Count: {count()}, Doubled: {doubled()}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
)
}
Angular Signals (v17+)
import { signal, computed, effect } from '@angular/core'
@Component({
template: `
<p>Count: {{ count() }}, Doubled: {{ doubled() }}</p>
<button (click)="increment()">+1</button>
`
})
class CounterComponent {
count = signal(0)
doubled = computed(() => this.count() * 2)
constructor() {
effect(() => {
console.log(`Count: ${this.count()}`)
})
}
increment() {
this.count.update(c => c + 1)
}
}
ReactとSignalsの関係(かんけい)
Reactチームは公式(こうしき)にはSignalsを採用(さいよう)していません。代(か)わりに、React 19のコンパイラ(React Forget)が同様(どうよう)の最適化(さいてきか)を自動的(じどうてき)に実行(じっこう)します。つまり、Reactは**コンパイラベースの最適化(さいてきか)**を選択(せんたく)しました。
| アプローチ | フレームワーク | 原理(げんり) |
|---|---|---|
| Signals | Preact, Solid, Angular | ランタイムで細粒度(さいりゅうど)の反応性追跡(ついせき) |
| Compiler | React 19 | ビルド時(じ)に自動(じどう)メモ化(か) |
| Proxy | MobX, Valtio | Proxyオブジェクトで変更検知(へんこうけんち) |
9. 総合比較表(そうごうひかくひょう)
機能比較(きのうひかく)
| 項目(こうもく) | Context | Redux TK | Zustand | Jotai | Recoil |
|---|---|---|---|---|---|
| バンドルサイズ | 0KB(内蔵(ないぞう)) | 11KB | 1.1KB | 3.4KB | 21KB |
| ボイラープレート | 中(ちゅう) | 低(てい)(RTK) | 非常(ひじょう)に低(てい) | 非常(ひじょう)に低(てい) | 中(ちゅう) |
| 学習曲線(がくしゅうきょくせん) | 低(てい) | 中(ちゅう) | 低(てい) | 低(てい) | 中(ちゅう) |
| TypeScript | 良好(りょうこう) | 非常(ひじょう)に良好(りょうこう) | 非常(ひじょう)に良好(りょうこう) | 非常(ひじょう)に良好(りょうこう) | 良好(りょうこう) |
| SSRサポート | 良好(りょうこう) | 良好(りょうこう) | 非常(ひじょう)に良好(りょうこう) | 非常(ひじょう)に良好(りょうこう) | 制限的(せいげんてき) |
| DevTools | React DT | Redux DT | Redux DT | Jotai DT | 制限的(せいげんてき) |
| ミドルウェア | なし | 豊富(ほうふ) | persist/devtools | ユーティリティ | なし |
| 選択的(せんたくてき)サブスクリプション | 不可(ふか) | useSelector | selectorFn | atom単位(たんい) | selector |
| Provider必要(ひつよう) | 必須(ひっす) | 必須(ひっす) | 不要(ふよう) | 任意(にんい) | 必須(ひっす) |
| 非同期(ひどうき)サポート | useEffect | createAsyncThunk | 手動(しゅどう) | Suspense | Suspense |
パフォーマンスベンチマーク(1,000項目(こうもく)リスト)
| シナリオ | Context | Redux TK | Zustand | Jotai |
|---|---|---|---|---|
| 単一(たんいつ)項目(こうもく)更新(こうしん) | 12.3ms | 2.1ms | 1.8ms | 0.9ms |
| 全(ぜん)リスト更新(こうしん) | 15.2ms | 8.7ms | 7.9ms | 8.2ms |
| メモリ使用量(しようりょう) | 基準(きじゅん) | +2.1MB | +0.3MB | +0.5MB |
| 初期(しょき)レンダリング | 45ms | 52ms | 44ms | 46ms |
10. プロジェクト別(べつ)選択(せんたく)ガイド
判断(はんだん)フレームワーク
プロジェクト規模は?
├── 小規模(しょうきぼ)(1-3人、シンプルUI)
│ ├── 状態が頻繁に変わりますか?
│ │ ├── Yes → Zustand
│ │ └── No → React Context
│ └── サーバーデータが中心ですか?
│ └── Yes → TanStack Queryだけで十分
│
├── 中規模(ちゅうきぼ)(4-10人、複雑なUI)
│ ├── 多くの独立した状態がありますか?
│ │ ├── Yes → Jotai
│ │ └── No → Zustand
│ └── サーバー状態が複雑ですか?
│ └── Yes → TanStack Query + Zustand/Jotai
│
└── 大規模(だいきぼ)(10人以上、エンタープライズ)
├── 既存のReduxコードがありますか?
│ ├── Yes → Redux Toolkitへマイグレーション
│ └── No → Zustand + TanStack Query
└── 厳格なアーキテクチャが必要ですか?
└── Yes → Redux Toolkit
シナリオ別(べつ)推奨(すいしょう)の組(く)み合(あ)わせ
| シナリオ | 推奨(すいしょう)の組(く)み合(あ)わせ |
|---|---|
| シンプルなランディングページ | React Context |
| SaaSダッシュボード | Zustand + TanStack Query |
| Eコマース | Jotai + TanStack Query |
| ソーシャルメディアアプリ | Zustand + TanStack Query |
| 大規模(だいきぼ)エンタープライズ | Redux Toolkit + RTK Query |
| リアルタイムコラボレーションツール | Zustand + WebSocketミドルウェア |
| エディタ/IDEツール | Jotai(細粒度(さいりゅうど)の状態制御(じょうたいせいぎょ)) |
11. マイグレーションガイド
ReduxからZustandへ
// Before: Redux Toolkit
// store.ts
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1 },
decrement: (state) => { state.value -= 1 },
},
})
// コンポーネント
function Counter() {
const count = useSelector((state: RootState) => state.counter.value)
const dispatch = useDispatch()
return <button onClick={() => dispatch(increment())}>Count: {count}</button>
}
// After: Zustand
const useCounterStore = create<CounterStore>((set) => ({
value: 0,
increment: () => set((s) => ({ value: s.value + 1 })),
decrement: () => set((s) => ({ value: s.value - 1 })),
}))
function Counter() {
const count = useCounterStore((s) => s.value)
const increment = useCounterStore((s) => s.increment)
return <button onClick={increment}>Count: {count}</button>
}
ContextからJotaiへ
// Before: React Context
const CountContext = createContext({ count: 0, setCount: () => {} })
function CountProvider({ children }) {
const [count, setCount] = useState(0)
return (
<CountContext.Provider value={{ count, setCount }}>
{children}
</CountContext.Provider>
)
}
function Counter() {
const { count, setCount } = useContext(CountContext)
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
}
// After: Jotai
const countAtom = atom(0)
function Counter() {
const [count, setCount] = useAtom(countAtom)
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
}
// Provider削除!コード50%削減!
マイグレーションチェックリスト
- 既存(きそん)の状態(じょうたい)をサーバー/クライアントに分類(ぶんるい)
- サーバー状態(じょうたい)をTanStack Queryに移行(いこう)
- クライアント状態(じょうたい)を選択(せんたく)したライブラリに移行(いこう)
- テストコードを更新(こうしん)
- パフォーマンスベンチマークを比較(ひかく)
- 段階的(だんかいてき)ロールアウト(機能(きのう)ごとのマイグレーション)
12. 面接質問(めんせつしつもん)とクイズ
面接質問(めんせつしつもん)10選(せん)
Q1. サーバー状態(じょうたい)とクライアント状態(じょうたい)の違(ちが)いを説明(せつめい)し、それぞれに適(てき)したツールを推奨(すいしょう)してください。
サーバー状態(じょうたい)はAPIから取得(しゅとく)する非同期(ひどうき)データで、キャッシング、無効化(むこうか)、同期(どうき)が核心(かくしん)です。TanStack QueryやSWRが適切(てきせつ)です。クライアント状態(じょうたい)はUIトグルやフォーム入力(にゅうりょく)など純粋(じゅんすい)なフロントエンド状態(じょうたい)で、Zustand、Jotai、Contextが適切(てきせつ)です。
Q2. React Contextのパフォーマンス問題(もんだい)は何(なん)ですか、どのように解決(かいけつ)しますか?
Contextのvalueが変更(へんこう)されると、そのContextをサブスクライブする全(すべ)てのコンポーネントが再(さい)レンダリングされます。解決策(かいけつさく)はContextの分割(ぶんかつ)、useMemoでのvalue メモ化(か)、またはZustand/Jotaiへの移行(いこう)です。
Q3. ZustandとJotaiの核心的(かくしんてき)な違(ちが)いは何(なん)ですか?
Zustandはストアベース(トップダウン)アプローチで、関連(かんれん)する状態(じょうたい)を1つのストアにまとめます。Jotaiはatomベース(ボトムアップ)アプローチで、状態(じょうたい)を最小単位(さいしょうたんい)に分割(ぶんかつ)します。単一(たんいつ)ストアが自然(しぜん)な場合(ばあい)はZustand、多(おお)くの独立(どくりつ)した状態(じょうたい)がある場合(ばあい)はJotaiが適切(てきせつ)です。
Q4. Redux Toolkitが2025年(ねん)でも有効(ゆうこう)な理由(りゆう)を説明(せつめい)してください。
厳格(げんかく)なパターンの強制(きょうせい)、タイムトラベルデバッグ、豊富(ほうふ)なミドルウェアエコシステム、内蔵(ないぞう)RTK Query、大規模(だいきぼ)チームでのコード一貫性(いっかんせい)の維持(いじ)が利点(りてん)です。
Q5. Stale-While-Revalidateパターンを説明(せつめい)してください。
キャッシュされた(stale)データを即座(そくざ)に返(かえ)して高速(こうそく)なUXを提供(ていきょう)しつつ、バックグラウンドで新(あたら)しいデータを取得(しゅとく)し(revalidate)、UIを更新(こうしん)します。TanStack QueryとSWRの核心戦略(かくしんせんりゃく)です。
Q6. SignalsはReactの再(さい)レンダリングモデルとどう違(ちが)いますか?
Reactは状態変更時(じょうたいへんこうじ)にコンポーネント関数全体(かんすうぜんたい)を再実行(さいじっこう)し、Virtual DOMを比較(ひかく)します。Signalsは変更(へんこう)された値(あたい)を使用(しよう)するDOM部分(ぶぶん)のみを直接更新(ちょくせつこうしん)し、不要(ふよう)なコンポーネント再実行(さいじっこう)を回避(かいひ)します。
Q7. ZustandのpersistミドルウェアがSSRで引(ひ)き起(お)こす問題(もんだい)とその解決法(かいけつほう)は?
サーバーにはlocalStorageが存在(そんざい)しないため、hydration mismatchが発生(はっせい)します。skipHydrationオプションやuseEffect内(ない)での手動(しゅどう)hydrationで解決(かいけつ)します。
Q8. JotaiのderiveされたatomとRecoilのselectorの違(ちが)いを説明(せつめい)してください。
機能的(きのうてき)には類似(るいじ)していますが、Jotaiは文字列(もじれつ)keyが不要(ふよう)で、バンドルサイズが6倍(ばい)小(ちい)さいです。Jotai atomは参照同一性(さんしょうどういつせい)で識別(しきべつ)されるため、keyの衝突(しょうとつ)リスクがありません。
Q9. TanStack QueryのqueryKey設計戦略(せっけいせんりゃく)は?
階層的(かいそうてき)なキー構造(こうぞう)(例:['users', userId, 'posts'])を使用(しよう)して細粒度(さいりゅうど)のキャッシュ無効化(むこうか)を可能(かのう)にします。親(おや)キーを無効化(むこうか)すると子(こ)キーも自動的(じどうてき)に無効化(むこうか)されます。
Q10. 新(あたら)しいプロジェクトで状態管理(じょうたいかんり)ツールを選択(せんたく)する基準(きじゅん)は?
プロジェクト規模(きぼ)、チーム経験(けいけん)、SSR要件(ようけん)、バンドルサイズ制約(せいやく)、状態(じょうたい)の複雑度(ふくざつど)を考慮(こうりょ)します。ほとんどのプロジェクトはTanStack Query + Zustandの組(く)み合(あ)わせから始(はじ)めるのが良(よ)いでしょう。
クイズ5問(もん)
Q1. ZustandでProviderが不要(ふよう)な理由(りゆう)は?
Zustandはモジュールスコープでストアを作成(さくせい)し、グローバルシングルトンとして管理(かんり)します。Reactコンポーネントツリーの外部(がいぶ)で状態(じょうたい)を管理(かんり)するため、Providerでラップする必要(ひつよう)がありません。これにより設定(せってい)が簡素化(かんそか)され、Providerのネスト問題(もんだい)が排除(はいじょ)されます。
Q2. TanStack QueryのstaleTimeとgcTimeの違(ちが)いは?
staleTimeはデータがfresh状態(じょうたい)を維持(いじ)する時間(じかん)です。freshデータは再取得(さいしゅとく)されません。gcTime(ガベージコレクション時間(じかん))は非(ひ)アクティブなクエリデータがメモリから削除(さくじょ)されるまでの時間(じかん)です。デフォルト値(ち)はstaleTimeが0、gcTimeが5分(ふん)です。
Q3. JotaiのatomがRecoilのatomより優(すぐ)れている点(てん)を2つ挙(あ)げてください。
第一(だいいち)に、文字列(もじれつ)keyが不要(ふよう)です。Jotai atomはJavaScriptオブジェクト参照(さんしょう)で識別(しきべつ)されるため、keyの衝突(しょうとつ)リスクがありません。第二(だいに)に、バンドルサイズが3.4KBでRecoilの21KBの約(やく)6分(ぶん)の1です。
Q4. useMemoだけでReact Contextの再(さい)レンダリング問題(もんだい)を完全(かんぜん)に解決(かいけつ)できますか?
いいえ。useMemoはvalueオブジェクトの不要(ふよう)な再生成(さいせいせい)を防(ふせ)ぎますが、オブジェクト内(ない)の実際(じっさい)の値(あたい)が変更(へんこう)されると、依然(いぜん)として全(すべ)ての消費者(しょうひしゃ)が再(さい)レンダリングされます。Contextを分割(ぶんかつ)するか、選択的(せんたくてき)サブスクリプションをサポートするライブラリ(Zustand、Jotai)を使用(しよう)する必要(ひつよう)があります。
Q5. Redux ToolkitからZustandへマイグレーションする際(さい)の最(もっと)も重要(じゅうよう)な注意点(ちゅういてん)は?
ZustandにはReduxのミドルウェアチェーンと同(おな)じ概念(がいねん)がないため、sagaや複雑(ふくざつ)な非同期(ひどうき)フローを再設計(さいせっけい)する必要(ひつよう)があります。また、Redux DevToolsのタイムトラベルデバッグ機能(きのう)はZustandでは制限的(せいげんてき)にしか使用(しよう)できません。段階的(だんかいてき)マイグレーションのために、両方(りょうほう)のライブラリを一時的(いちじてき)に並行運用(へいこううんよう)する戦略(せんりゃく)が必要(ひつよう)です。
参考資料(さんこうしりょう)
- Zustand GitHub Repository
- Jotai Documentation
- Redux Toolkit Official Docs
- TanStack Query Documentation
- Recoil Documentation
- Preact Signals
- SolidJS Reactivity
- Angular Signals RFC
- SWR Documentation
- React Documentation - Context
- npm trends: State Management Libraries
- Theo Browne - State Management in 2025
- Jack Herrington - Zustand vs Jotai
- TkDodo - Practical React Query