はじめに
Reactで状態管理を行う際、useStateとuseReducerのどちらを使うべきか迷ったことはありませんか。両方とも状態管理のためのフックですが、それぞれ適した場面が異なります。
この記事では、以下の内容を解説します。
-
useStateとuseReducerの基本的な使い方 - 2つのフックの違いと特徴
- 実践的な使い分けの判断基準
- 具体的なコード例
初心者の方でも理解できるよう、図解を交えながら説明していきますね。
useStateの基本
useStateとは
useStateは、コンポーネント内でシンプルな状態を管理するための最も基本的なフックです。状態の値と、その値を更新する関数のペアを返します。
基本的な使い方
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>
+1
</button>
</div>
);
}
構文の説明
-
useState(0): 初期値を0として状態を初期化 -
count: 現在の状態値 -
setCount: 状態を更新するための関数
状態更新は非常にシンプルで、新しい値をsetCountに渡すだけです。
useReducerの基本
useReducerとは
useReducerは、より複雑な状態管理ロジックを扱うためのフックです。Reduxに似た考え方で、アクションに基づいて状態を更新します。
基本的な使い方
import { useReducer } from 'react';
// Reducer関数: 現在の状態とアクションを受け取り、新しい状態を返す
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>カウント: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
+1
</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>
-1
</button>
<button onClick={() => dispatch({ type: 'RESET' })}>
リセット
</button>
</div>
);
}
構文の説明
-
counterReducer: 状態更新のロジックを定義する関数 -
state: 現在の状態 -
dispatch: アクションを送信する関数 -
action: 状態をどのように変更するかを指示するオブジェクト
2つのフックの違い
状態更新の仕組みの違い
コードの複雑さの比較
useState
- 状態更新ロジックがコンポーネント内に分散する
- シンプルな状態には最適
- コード量が少ない
useReducer
- 状態更新ロジックが1箇所に集約される
- 複雑な状態管理に向いている
- テストしやすい
使い分けの判断基準
以下の判断フローチャートを参考にしてください。
useStateが適している場面
以下のような場合はuseStateを選択しましょう。
-
単純な値の管理
- 文字列、数値、真偽値などのプリミティブ型
- 独立した状態
-
更新ロジックがシンプル
- 単純な代入や計算のみ
- 条件分岐が少ない
-
状態が1つまたは2つ程度
- 関連性の低い複数の状態
具体例
- トグルボタンの開閉状態
- 入力フィールド1つの値
- シンプルなカウンター
useReducerが適している場面
以下のような場合はuseReducerを選択しましょう。
-
複雑な状態オブジェクト
- ネストした構造
- 複数のプロパティを持つオブジェクト
-
状態間に依存関係がある
- ある状態の更新が他の状態に影響する
- 複数の状態を同時に更新する必要がある
-
更新ロジックが複雑
- 多くの条件分岐
- ビジネスロジックが含まれる
-
状態更新のパターンが多い
- 様々なアクションタイプがある
- 状態更新の再利用性が必要
具体例
- フォームの管理(複数の入力項目、バリデーション)
- ショッピングカート
- 複雑なUIコンポーネントの状態
実践例:シンプルなカウンター(useState)
単純な増減だけを行うカウンターはuseStateが適しています。
import { useState } from 'react';
function SimpleCounter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return (
<div>
<h2>シンプルなカウンター</h2>
<p>現在の値: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
);
}
このコードのポイント
- 状態は数値1つだけ
- 更新ロジックは単純な加算・減算
- コードが短く読みやすい
実践例:複雑なフォーム管理(useReducer)
複数の入力項目とバリデーションを持つフォームはuseReducerが適しています。
import { useReducer } from 'react';
// 初期状態
const initialState = {
username: '',
email: '',
password: '',
errors: {},
isSubmitting: false
};
// Reducer関数
function formReducer(state, action) {
switch (action.type) {
case 'UPDATE_FIELD':
return {
...state,
[action.field]: action.value,
errors: {
...state.errors,
[action.field]: '' // エラーをクリア
}
};
case 'SET_ERROR':
return {
...state,
errors: {
...state.errors,
[action.field]: action.error
}
};
case 'SET_SUBMITTING':
return {
...state,
isSubmitting: action.value
};
case 'RESET_FORM':
return initialState;
default:
return state;
}
}
function RegistrationForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleChange = (field) => (e) => {
dispatch({
type: 'UPDATE_FIELD',
field,
value: e.target.value
});
};
const validateForm = () => {
let isValid = true;
if (state.username.length < 3) {
dispatch({
type: 'SET_ERROR',
field: 'username',
error: 'ユーザー名は3文字以上必要です'
});
isValid = false;
}
if (!state.email.includes('@')) {
dispatch({
type: 'SET_ERROR',
field: 'email',
error: '有効なメールアドレスを入力してください'
});
isValid = false;
}
if (state.password.length < 8) {
dispatch({
type: 'SET_ERROR',
field: 'password',
error: 'パスワードは8文字以上必要です'
});
isValid = false;
}
return isValid;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) return;
dispatch({ type: 'SET_SUBMITTING', value: true });
// 送信処理(API呼び出しなど)
try {
// await submitForm(state);
console.log('フォーム送信:', state);
dispatch({ type: 'RESET_FORM' });
} catch (error) {
console.error('送信エラー:', error);
} finally {
dispatch({ type: 'SET_SUBMITTING', value: false });
}
};
return (
<form onSubmit={handleSubmit}>
<h2>ユーザー登録フォーム</h2>
<div>
<label>
ユーザー名:
<input
type="text"
value={state.username}
onChange={handleChange('username')}
/>
</label>
{state.errors.username && (
<p style={{ color: 'red' }}>{state.errors.username}</p>
)}
</div>
<div>
<label>
メールアドレス:
<input
type="email"
value={state.email}
onChange={handleChange('email')}
/>
</label>
{state.errors.email && (
<p style={{ color: 'red' }}>{state.errors.email}</p>
)}
</div>
<div>
<label>
パスワード:
<input
type="password"
value={state.password}
onChange={handleChange('password')}
/>
</label>
{state.errors.password && (
<p style={{ color: 'red' }}>{state.errors.password}</p>
)}
</div>
<button type="submit" disabled={state.isSubmitting}>
{state.isSubmitting ? '送信中...' : '登録'}
</button>
</form>
);
}
このコードのポイント
- 複数の関連する状態をオブジェクトで管理
- 状態更新のロジックがReducer関数に集約されている
- エラー状態と送信状態も統合的に管理
- アクションタイプで更新の意図が明確になっている
useStateで実装した場合との比較
もしuseStateで同じフォームを実装すると、以下のようになります。
// useStateの場合(推奨されない例)
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
// 複数のsetState呼び出しが必要
const handleChange = (field) => (e) => {
if (field === 'username') setUsername(e.target.value);
if (field === 'email') setEmail(e.target.value);
if (field === 'password') setPassword(e.target.value);
// エラーもクリアする必要がある
setErrors({ ...errors, [field]: '' });
};
状態が増えるほど管理が煩雑になることが分かりますね。
パフォーマンスの考慮点
再レンダリングの影響
useStateとuseReducerのどちらも、状態が更新されるとコンポーネントが再レンダリングされます。パフォーマンスの観点では、両者に大きな差はありません。
useCallbackとの組み合わせ
子コンポーネントに状態更新関数を渡す場合、useCallbackと組み合わせることで不要な再レンダリングを防げます。
import { useReducer, useCallback } from 'react';
function Parent() {
const [state, dispatch] = useReducer(reducer, initialState);
// dispatchは安定した参照を持つため、useCallbackは不要
// しかし、追加のロジックを含む関数はメモ化が有効
const handleIncrement = useCallback(() => {
dispatch({ type: 'INCREMENT' });
}, []);
return <Child onIncrement={handleIncrement} />;
}
まとめ
使い分けのポイント再確認
useStateを選ぶ場合
- 単純な値の管理
- 独立した状態
- 更新ロジックがシンプル
- 素早くプロトタイプを作りたい時
useReducerを選ぶ場合
- 複雑な状態オブジェクト
- 複数の関連する状態
- 状態更新のロジックが複雑
- テストしやすさが重要
- 状態更新のパターンが多い