Reactアプリケーションにおいて、状態管理はアプリの規模や複雑性に応じて適切なアプローチを選択する必要があります。本章では、代表的な状態管理の選択肢と、型安全性を活用した設計、およびサーバーサイドとのデータ連携について解説します。
1. Context API vs Redux vs Zustand などの選択肢
状態管理には多くの選択肢があります。それぞれの特徴を以下にまとめます。
Context API
- 特徴: React標準の状態管理手段。
- 利点:
- 外部ライブラリが不要。
- シンプルなデータ共有に最適。
- 欠点:
- 複雑な状態管理には不向き。
- 再レンダリングのパフォーマンス問題が発生しやすい。
Redux
- 特徴: アプリ全体で状態を一元管理。
- 利点:
- 拡張性とプラグインエコシステムの充実。
- 状態の追跡やデバッグが容易 (Redux DevTools)。
- 欠点:
- 初期設定が複雑。
- 設定作業が冗長になりがち。
Zustand
- 特徴: 軽量かつ直感的な状態管理ライブラリ。
- 利点:
- シンプルでボイラープレートが少ない。
- Context APIの欠点を回避。
- 欠点:
- 大規模アプリケーションには向かない場合がある。
どれを選ぶべきか?
- 小規模なアプリや局所的な状態管理 → Context API
- 複雑なビジネスロジックや大規模アプリ → Redux
- シンプルで軽量な状態管理を優先 → Zustand
2. 型安全な状態管理の設計 (TypeScriptによる型付け)
状態管理において型安全性を確保することで、保守性が向上しバグのリスクを軽減できます。以下はTypeScriptを使用した状態管理の例です。
Context APIの型付け
tsx
import React, { createContext, useContext, useState } from "react";
interface AppState {
user: string;
isLoggedIn: boolean;
}
interface AppContextProps {
state: AppState;
setState: React.Dispatch<React.SetStateAction<AppState>>;
}
const AppContext = createContext<AppContextProps | undefined>(undefined);
export const AppProvider: React.FC = ({ children }) => {
const [state, setState] = useState<AppState>({ user: "", isLoggedIn: false });
return (
<AppContext.Provider value={{ state, setState }}>
{children}
</AppContext.Provider>
);
};
export const useAppContext = () => {
const context = useContext(AppContext);
if (!context) throw new Error("useAppContext must be used within AppProvider");
return context;
};
Redux Toolkitの型付け
tsx
import { configureStore, createSlice, PayloadAction } from "@reduxjs/toolkit";
interface AppState {
user: string;
isLoggedIn: boolean;
}
const initialState: AppState = { user: "", isLoggedIn: false };
const appSlice = createSlice({
name: "app",
initialState,
reducers: {
login: (state, action: PayloadAction<string>) => {
state.user = action.payload;
state.isLoggedIn = true;
},
logout: (state) => {
state.user = "";
state.isLoggedIn = false;
},
},
});
export const { login, logout } = appSlice.actions;
export const store = configureStore({ reducer: appSlice.reducer });
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
3. 複雑な状態を扱う際のベストプラクティス
状態のスコープを限定する:
- 状態は必要なコンポーネントにのみ提供する。
- グローバル状態とローカル状態を使い分ける。
不変性を維持する:
- 状態を直接変更せず、
setState
やdispatch
を使用する。
- 状態を直接変更せず、
メモ化を活用する:
useMemo
やuseCallback
を使用して不要な再レンダリングを防止。
型安全性を重視する:
- TypeScriptの型チェックで意図しないバグを防ぐ。
依存関係を最小限に:
- 必要以上に多くの状態を1つにまとめない。
4. サーバーからのデータ取得と状態管理の統合
React Queryの活用
React Queryは、サーバーからのデータ取得とキャッシュ管理を効率的に行えるライブラリです。
tsx
import { useQuery } from "react-query";
const fetchUser = async (): Promise<User> => {
const response = await fetch("/api/user");
if (!response.ok) throw new Error("Network response was not ok");
return response.json();
};
const UserProfile: React.FC = () => {
const { data, error, isLoading } = useQuery("user", fetchUser);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Welcome, {data.name}!</div>;
};
RTK Queryの活用
Redux Toolkit Query (RTK Query) を使うと、Reduxの状態管理とサーバーサイドデータの統合が容易になります。
tsx
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
interface User {
id: string;
name: string;
}
const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
endpoints: (builder) => ({
getUser: builder.query<User, void>({ query: () => "user" }),
}),
});
export const { useGetUserQuery } = api;
const UserProfile: React.FC = () => {
const { data, error, isLoading } = useGetUserQuery();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Welcome, {data?.name}!</div>;
};
これらのアプローチを理解し、プロジェクトの規模や特性に応じて選択することで、Reactアプリケーションの状態管理をより効率的に設計できます。