React useReducer 実務での使い方|複雑な状態管理を実装するパターン集
Reactで状態管理を行う際、useStateが基本となりますが、複数の関連する状態を扱う場合や状態遷移が複雑な場合にはuseReducerが活躍します。本記事では、実務で頻繁に遭遇する状況を想定した、実践的なuseReducerの使い方を解説します。
useReducerの基本的な考え方
useReducerはシンプルに言えば、状態と行動(Action)を組み合わせて新しい状態を作り出す関数です。複数の関連する状態をまとめて管理できるため、状態間の関連性が強い場合に有効です。
// 基本的な使い方
const [state, dispatch] = useReducer(reducer, initialState);
// reducerは以下のような形
function reducer(state: State, action: Action): State {
switch(action.type) {
case 'ACTION_TYPE':
return { ...state, /* 変更内容 */ };
default:
return state;
}
}
実務で遭遇する複雑な状態管理
それでは、実務で実際に使用しているパターンを紹介します。
1. フォーム入力と検証エラーの管理
ユーザー情報編集フォームのような、複数のフィールドと対応する検証エラーを管理する場合、useReducerが有効です。
// ユーザーフォーム管理の実装例
interface FormState {
values: {
name: string;
email: string;
phone: string;
address: string;
};
errors: {
name?: string;
email?: string;
phone?: string;
address?: string;
};
touched: {
name: boolean;
email: boolean;
phone: boolean;
address: boolean;
};
isSubmitting: boolean;
submitError?: string;
}
type FormAction =
| { type: 'CHANGE_FIELD'; payload: { field: keyof FormState['values']; value: string } }
| { type: 'BLUR_FIELD'; payload: { field: keyof FormState['values'] } }
| { type: 'SET_ERRORS'; payload: Record }
| { type: 'SUBMIT_START' }
| { type: 'SUBMIT_SUCCESS' }
| { type: 'SUBMIT_ERROR'; payload: string }
| { type: 'RESET_FORM' };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'CHANGE_FIELD':
return {
...state,
values: {
...state.values,
[action.payload.field]: action.payload.value,
},
};
case 'BLUR_FIELD':
return {
...state,
touched: {
...state.touched,
[action.payload.field]: true,
},
};
case 'SET_ERRORS':
return {
...state,
errors: action.payload,
};
case 'SUBMIT_START':
return {
...state,
isSubmitting: true,
submitError: undefined,
};
case 'SUBMIT_SUCCESS':
return {
...state,
isSubmitting: false,
values: initialFormState.values,
touched: initialFormState.touched,
errors: {},
};
case 'SUBMIT_ERROR':
return {
...state,
isSubmitting: false,
submitError: action.payload,
};
case 'RESET_FORM':
return initialFormState;
default:
return state;
}
}
const initialFormState: FormState = {
values: { name: '', email: '', phone: '', address: '' },
errors: {},
touched: { name: false, email: false, phone: false, address: false },
isSubmitting: false,
};
// コンポーネント内での使用
function UserEditForm() {
const [state, dispatch] = useReducer(formReducer, initialFormState);
const handleChange = (e: React.ChangeEvent) => {
const { name, value } = e.currentTarget;
dispatch({
type: 'CHANGE_FIELD',
payload: { field: name as keyof FormState['values'], value },
});
};
const handleBlur = (e: React.FocusEvent) => {
const { name } = e.currentTarget;
dispatch({
type: 'BLUR_FIELD',
payload: { field: name as keyof FormState['values'] },
});
};
const validateForm = () => {
const newErrors: Record = {};
if (!state.values.name.trim()) newErrors.name = '名前は必須です';
if (!state.values.email.match(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/)) {
newErrors.email = 'メールアドレスが無効です';
}
dispatch({ type: 'SET_ERRORS', payload: newErrors });
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
dispatch({ type: 'SUBMIT_START' });
try {
await fetch('/api/users', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(state.values),
});
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (error) {
dispatch({
type: 'SUBMIT_ERROR',
payload: '送信に失敗しました。もう一度お試しください。',
});
}
};
return (
);
}
2. 非同期データ取得とローディング状態の管理
APIからデータを取得する際、ローディング状態、成功時のデータ、エラーメッセージなど複数の状態が必要になります。これらをuseReducerで統括管理するのが実務的です。
// データ取得管理の実装例
interface FetchState {
data: T | null;
loading: boolean;
error: string | null;
lastFetched: Date | null;
retryCount: number;
}
interface User {
id: number;
name: string;
email: string;
role: string;
}
type FetchAction =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: T }
| { type: 'FETCH_ERROR'; payload: string }
| { type: 'RETRY' }
| { type: 'RESET' };
function fetchReducer(state: FetchState, action: FetchAction): FetchState {
switch (action.type) {
case 'FETCH_START':
return {
...state,
loading: true,
error: null,
};
case 'FETCH_SUCCESS':
return {
...state,
data: action.payload,
loading: false,
error: null,
lastFetched: new Date(),
retryCount: 0,
};
case 'FETCH_ERROR':
return {
...state,
loading: false,
error: action.payload,
retryCount: state.retryCount + 1,
};
case 'RETRY':
return {
...state,
error: null,
loading: true,
};
case 'RESET':
return {
data: null,
loading: false,
error: null,
lastFetched: null,
retryCount: 0,
};
default:
return state;
}
}
// コンポーネント内での使用
function UserList() {
const [state, dispatch] = useReducer(fetchReducer, {
data: null,
loading: false,
error: null,
lastFetched: null,
retryCount: 0,
});
useEffect(() => {
const fetchUsers = async () => {
dispatch({ type: 'FETCH_START' });
try {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('ユーザー取得に失敗しました');
const data = await response.json();
dispatch({ type: 'FETCH_SUCCESS', payload: data });
} catch (error) {
dispatch({
type: 'FETCH_ERROR',
payload: error instanceof Error ? error.message : '不明なエラー',
});
}
};
fetchUsers();
}, []);
const handleRetry = () => {
dispatch({ type: 'RETRY' });
// 再度fetch処理を実行
};
if (state.loading) return 読み込み中...
;
if (state.error) {
return (
{state.error}
);
}
return (
{state.data?.map((user) => (
- {user.name} - {user.email}
))}
);
}
実務でよくある応用パターン
3. ページング + フィルタリングの状態管理
リスト表示ページでページングやフィルタリング機能を実装する際、複数の状態を一元管理するのが重要です。
interface ListState {
items: T[];
totalCount: number;
currentPage: number;
pageSize: number;
filter: {
searchQuery: string;
sortBy: 'name' | 'date' | 'relevance';
sortOrder: 'asc' | 'desc';
};
loading: boolean;
error: string | null;
}
type ListAction =
| { type: 'SET_ITEMS'; payload: { items: T[]; totalCount: number } }
| { type: 'NEXT_PAGE' }
| { type: 'PREV_PAGE' }
| { type: 'GO_TO_PAGE'; payload: number }
| { type: 'SET_SEARCH'; payload: string }
| { type: 'SET_SORT'; payload: { sortBy: string; sortOrder: 'asc' | 'desc' } }
| { type: 'LOADING' }
| { type: 'ERROR'; payload: string }
| { type: 'RESET' };
function listReducer(state: ListState, action: ListAction): ListState {
switch (action.type) {
case 'SET_ITEMS':
return {
...state,
items: action.payload.items,
totalCount: action.payload.totalCount,
loading: false,
error: null,
};
case 'NEXT_PAGE':
return {
...state,
currentPage: Math.min(
state.currentPage + 1,
Math.ceil(state.totalCount / state.pageSize)
),
};
case 'PREV_PAGE':
return {
...state,
currentPage: Math.max(state.currentPage - 1, 1),
};
case 'GO_TO_PAGE':
return {
...state,
currentPage: Math.max(1, action.payload),
};
case 'SET_SEARCH':
return {
...state,
filter: { ...state.filter, searchQuery: action.payload },
currentPage: 1, // 検索時はページを1に戻す
};
case 'SET_SORT':
return {
...state,
filter: {
...state.filter,
sortBy: action.payload.sortBy as any,
sortOrder: action.payload.sortOrder,
},
};
case 'LOADING':
return { ...state, loading: true };
case 'ERROR':
return { ...state, loading: false, error: action.payload };
case 'RESET':
return {
items: [],
totalCount: 0,
currentPage: 1,
pageSize: 10,
filter: { searchQuery: '', sortBy: 'name', sortOrder: 'asc' },
loading: false,
error: null,
};
default:
return state;
}
}
4. 複雑なワークフロー状態の管理
例えば、注文処理のような複数ステップを持つワークフローでは、状態遷移ルールを厳密に定義することが重要です。
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
interface OrderState {
status: OrderStatus;
items: OrderItem[];
totalAmount: number;
paymentMethod?: string;
trackingNumber?: string;
estimatedDelivery?: Date;
cancellationReason?: string;
history: Array<{ status: OrderStatus; timestamp: Date }>;
}
interface OrderItem {
id: string;
name: string;
quantity: number;
price: number;
}
type OrderAction =
| { type: 'CREATE_ORDER'; payload: OrderItem[] }
| { type: 'PROCESS_PAYMENT'; payload: string }
| { type: 'SHIP_ORDER'; payload: { trackingNumber: string; estimatedDelivery: Date } }
| { type: 'DELIVER_ORDER' }
| { type: 'CANCEL_ORDER'; payload: string }
| { type: 'LOAD_ORDER'; payload: OrderState };
function orderReducer(state: OrderState, action: OrderAction): OrderState {
const addHistory = (newStatus: OrderStatus) => [
...state.history,
{ status: newStatus, timestamp: new Date() },
];
switch (action.type) {
case 'CREATE_ORDER':
return {
status: 'pending',
items: action.payload,
totalAmount: action.payload.reduce((sum, item) => sum + item.price * item.quantity, 0),
history: [{ status: 'pending', timestamp: new Date() }],
};
case 'PROCESS_PAYMENT':
if (state.status !== 'pending') {
throw new Error('Invalid state transition: payment can only be processed from pending');
}
return {
...state,
status: 'processing',
paymentMethod: action.payload,
history: addHistory('processing'),
};
case 'SHIP_ORDER':
if (state.status !== 'processing') {
throw new Error('Invalid state transition: can only ship from processing');
}
return {
...state,
status: 'shipped',
trackingNumber: action.payload.trackingNumber,
estimatedDelivery: action.payload.estimatedDelivery,
history: addHistory('shipped'),
};
case 'DELIVER_ORDER':
if (state.status !== 'shipped') {
throw new Error('Invalid state transition: can only deliver from shipped');
}
return {
...state,
status: 'delivered',
history: addHistory('delivered'),
};
case 'CANCEL_ORDER':
if (!['pending', 'processing'].includes(state.status)) {
throw new Error('Invalid state transition: can only cancel from pending or processing');
}
return {
...state,
status: 'cancelled',
cancellationReason: action.payload,
history: addHistory('cancelled'),
};
case 'LOAD_ORDER':
return action.payload;
default:
return state;
}
}
useReducerの注意点と実務的なTips
エラーハンドリング
useReducer内でエラーをスローする際は注意が必要です。状態遷移ルール違反はtry-catchで適切に処理しましょう。
function OrderComponent() {
const [state, dispatch] = useReducer(orderReducer, initialOrderState);
const handlePayment = async (paymentMethod: string) => {
try {
dispatch({ type: 'PROCESS_PAYMENT', payload: paymentMethod });
// API呼び出しなど
} catch (error) {
console.error('Payment processing failed:', error);
// ユーザーにエラー通知
}
};
}
パフォーマンス最適化
useCallbackと組み合わせて、dispatch関数を安定化することで不要な再レンダリングを防ぎます。
function SearchComponent() {
const [state, dispatch] = useReducer(listReducer, initialState);
const handleSearch = useCallback(
(query: string) => {
dispatch({ type: 'SET_SEARCH', payload: query });
},
[]
);
return ;
}
DevTools連携
複雑な状態管理では、Redux DevToolsの活用が便利です。Chrome拡張機能で状態遷移を可視化できます。
// Redux DevToolsの基本的な活用
const composeEnhancers =
typeof window === 'object' && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
: () => (f: any) => f;
// useReducerそのものではなく、グローバル状態管理と組み合わせる際に有効
useState vs useReducer の選択基準
実務では以下のような基準で使い分けています。
- useStateを選ぶ場合:単純な状態(toggle、単一の値など)、状態間の関連性が低い場合
- useReducerを選ぶ場合:複数の関連した状態、状態遷移が複雑、アンドゥ・リドゥが必要、テストが重要な場合
実務的なベストプラクティス
1. Action型を厳密に定義
TypeScriptを活用して、型安全なActionを定義することで、ランタイムエラーを未然に防げます。
2. Reducerは純粋関数に
副作用を持たず、同じInputに対して常に同じOutputを返すように実装します。
3. Action Creatorの活用
// Action Creatorでボイラープレートを削減
const formActions = {
changeField: (field: string, value: string) => ({
type: 'CHANGE_FIELD' as const,
payload: { field, value },
}),
submitStart: () => ({ type: 'SUBMIT_START' as const }),
submitSuccess: () => ({ type: 'SUBMIT_SUCCESS' as const }),
submitError: (error: string) => ({
type: 'SUBMIT_ERROR' as const,
payload: error,
}),
};
// 使用例
dispatch(formActions.changeField('email', 'user@example.com'));
4. Custom Hooks化による再利用性向上
// フォーム管理をCustom Hookにまとめる
function useForm>(initialValues: T) {
const [state, dispatch] = useReducer(formReducer, {
values: initialValues,
errors: {},
touched: Object.keys(initialValues).reduce((acc, key) => ({
...acc,
[key]: false,
}), {}),
isSubmitting: false,
});
return {
values: state.values,
errors: state.errors,
touched: state.touched,
isSubmitting: state.isSubmitting,
handleChange: (e: React.ChangeEvent) => {
dispatch({
type: 'CHANGE_FIELD',
payload: { field: e.target.name, value: e.target.value },
});
},
handleBlur: (e: React.FocusEvent) => {
dispatch({
type: 'BLUR_FIELD',
payload: { field: e.target.name },
});
},
setErrors: (errors: Record) => {
dispatch({ type: 'SET_ERRORS', payload: errors });
},
};
}
// コンポーネントでの使用
function MyForm() {
const form = useForm({ name: '', email: '' });
// form.values, form.handleChange など使用可能
}
まとめ
useReducerは複雑な状態管理が必要なコンポーネントで強力なツールです。実務では以下のポイントを意識して導入することが重要です。
- 複数の関連した状態をまとめて管理し、一貫性を保つ
- TypeScriptで型安全なAction定義を行う
- 状態遷移ルールを明確にして、無効な遷移を防ぐ
- Custom Hooksで再利用性を高める
- 過度に複雑化した場合は、Reduxなどの専門的なライブラリの導入を検討する
これらのパターンを習得することで、保守性の高いReactアプリケーションを構築できます。

