Reactの状態管理は、動的でインタラクティブなUIを構築する上で欠かせない概念です。useStateフックとTypeScriptを組み合わせることで、型安全で保守しやすい状態管理を実現できます。本記事では、シンプルなカウンターアプリから始めて、実践的な状態管理のパターンまで詳しく学習します。
Reactの状態(State)とは
状態の基本概念
状態(State)とは、コンポーネントが内部で管理するデータのことです。時間の経過やユーザーの操作によって変化し、UIの表示に影響を与えます。
状態の特徴:
- ミュータブル: 値を変更できる(Propsは変更不可)
- ローカル: 各コンポーネントが独自に管理
- リアクティブ: 状態が変わるとUIが自動更新
- イベント駆動: ユーザーの操作や非同期処理によって変化
useStateフックの基本構文
useStateは、関数コンポーネントで状態を管理するためのReactフックです:
import React, { useState } from 'react';
const [state, setState] = useState(initialValue);
state
: 現在の状態値setState
: 状態を更新する関数initialValue
: 状態の初期値
シンプルなカウンターを作る
基本的なカウンターコンポーネント
まず、最もシンプルなカウンターアプリを作成しましょう:
// src/components/Counter.tsx
import React, { useState } from 'react';
const Counter: React.FC = () => {
// 数値型の状態を定義
const [count, setCount] = useState(0);
// カウントを増やす関数
const increment = () => {
setCount(count + 1);
};
// カウントを減らす関数
const decrement = () => {
setCount(count - 1);
};
// カウントをリセットする関数
const reset = () => {
setCount(0);
};
return (
カウンター: {count}
+1 -1 リセット
); }; export default Counter;
TypeScriptの型推論の活用
useStateは型推論が働くため、シンプルなケースでは型注釈を省略できます:
// 型推論により自動的にnumber型として認識
const [count, setCount] = useState(0);
// 文字列の場合
const [name, setName] = useState(''); // string型として推論
// 真偽値の場合
const [isVisible, setIsVisible] = useState(false); // boolean型として推論
より実践的なカウンターアプリ
設定可能なカウンター
より高機能なカウンターを作成してみましょう:
// src/components/AdvancedCounter.tsx
import React, { useState } from 'react';
interface CounterSettings {
}
const AdvancedCounter: React.FC = () => {
const [count, setCount] = useState(0);
const [settings, setSettings] = useState({
});
const increment = () => {
setCount(prevCount => Math.min(prevCount + settings.step, settings.max));
};
const decrement = () => {
setCount(prevCount => Math.max(prevCount - settings.step, settings.min));
};
const updateStep = (newStep: number) => {
setSettings(prevSettings => ({
...prevSettings,
}));
};
return (
高機能カウンター: {count}
-{settings.step} = settings.max} > +{settings.step}
ステップ数:
複雑な状態管理パターン
オブジェクト型の状態管理
より複雑なデータ構造を状態として管理する方法:
// ユーザー情報を管理するコンポーネント
interface User {
};
}
const UserProfile: React.FC = () => {
const [user, setUser] = useState({
}
});
// 基本情報の更新
const updateBasicInfo = (field: keyof Pick, value: string | number) => {
setUser(prevUser => ({
...prevUser,
[field]: value
}));
};
// 設定の更新
const updatePreference = (
) => {
setUser(prevUser => ({
...prevUser,
...prevUser.preferences,
[key]: value
}
}));
};
return (
ユーザープロフィール
配列の状態管理
配列を状態として管理するTodoリストの例:
interface TodoItem {
}
const TodoList: React.FC = () => {
const [todos, setTodos] = useState([]);
const [newTodoText, setNewTodoText] = useState('');
// 新しいTodoを追加
const addTodo = () => {
if (newTodoText.trim() === '') return;
const newTodo: TodoItem = {
};
setTodos(prevTodos => [...prevTodos, newTodo]);
setNewTodoText('');
};
// Todoの完了状態を切り替え
const toggleTodo = (id: number) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
// Todoを削除
const deleteTodo = (id: number) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
// 完了済みのTodoをクリア
const clearCompleted = () => {
setTodos(prevTodos => prevTodos.filter(todo => !todo.completed));
};
// 統計情報
const completedCount = todos.filter(todo => todo.completed).length;
const totalCount = todos.length;
return (
Todo リスト
フォームの状態管理
制御されたコンポーネント
フォームの入力値を状態で管理する方法:
interface FormData {
}
const RegistrationForm: React.FC = () => {
const [formData, setFormData] = useState({
});
const [errors, setErrors] = useState>({});
// フィールドの値を更新
const updateField = (
) => {
setFormData(prev => ({
...prev,
[field]: value
}));
// エラーをクリア
if (errors[field]) {
setErrors(prev => ({
...prev,
[field]: undefined
}));
}
};
// 興味のある分野の切り替え
const toggleInterest = (interest: string) => {
setFormData(prev => ({
...prev,
? prev.interests.filter(i => i !== interest)
: [...prev.interests, interest]
}));
};
// バリデーション
const validateForm = (): boolean => {
const newErrors: Partial = {};
if (!formData.name.trim()) {
newErrors.name = '名前は必須です';
}
if (!formData.email.trim()) {
newErrors.email = 'メールアドレスは必須です';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = '有効なメールアドレスを入力してください';
}
if (!formData.age || formData.age 非同期処理と状態管理
ローディング状態の管理
APIからデータを取得する際のローディング状態の管理:
interface User {
}
interface ApiState {
}
const UserList: React.FC = () => {
const [userState, setUserState] = useState>({
});
// ユーザーデータを取得
const fetchUsers = async () => {
setUserState(prev => ({
...prev,
}));
try {
// 模擬API呼び出し
await new Promise(resolve => setTimeout(resolve, 2000));
const users: User[] = [
{ id: 1, name: '田中太郎', email: 'tanaka@example.com' },
{ id: 2, name: '佐藤花子', email: 'sato@example.com' },
{ id: 3, name: '鈴木一郎', email: 'suzuki@example.com' }
];
setUserState({
});
} catch (error) {
setUserState(prev => ({
...prev,
}));
}
};
return (
ユーザー一覧
{userState.loading ? '読み込み中...' : 'ユーザーを取得'} {userState.error && (
エラー: {userState.error}
)} {userState.data && (
{userState.data.map(user => (
{user.name} - {user.email}
))}
)}
); };
パフォーマンスの最適化
useCallbackとuseMemoの活用
状態更新関数とコンポーネントの最適化:
import React, { useState, useCallback, useMemo } from 'react';
interface FilterableList {
}
const FilterableList: React.FC = ({ items }) => {
const [filter, setFilter] = useState('');
const [sortBy, setSortBy] = useState('name');
// フィルタリングとソートの処理をメモ化
const filteredAndSortedItems = useMemo(() => {
return items
.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase()) ||
item.category.toLowerCase().includes(filter.toLowerCase())
)
.sort((a, b) => a[sortBy].localeCompare(b[sortBy]));
}, [items, filter, sortBy]);
// イベントハンドラーをメモ化
const handleFilterChange = useCallback((newFilter: string) => {
setFilter(newFilter);
}, []);
const handleSortChange = useCallback((newSort: 'name' | 'category') => {
setSortBy(newSort);
}, []);
return (
よくあるパターンとベストプラクティス
状態の初期化パターン
関数を使った遅延初期化:
// 重い計算を伴う初期値
const ExpensiveComponent: React.FC = () => {
// 初期化時にのみ実行される
const [data, setData] = useState(() => {
console.log('重い初期化処理を実行');
return calculateInitialData();
});
// ...
};
// LocalStorageからの初期値読み込み
const PersistentCounter: React.FC = () => {
const [count, setCount] = useState(() => {
const saved = localStorage.getItem('count');
return saved ? parseInt(saved, 10) : 0;
});
// カウントが変更されたらLocalStorageに保存
React.useEffect(() => {
localStorage.setItem('count', count.toString());
}, [count]);
// ...
};
カスタムフックでの状態管理
再利用可能な状態ロジックをカスタムフックとして抽出:
// カウンター用のカスタムフック
function useCounter(initialValue: number = 0, step: number = 1) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => {
setCount(prev => prev + step);
}, [step]);
const decrement = useCallback(() => {
setCount(prev => prev - step);
}, [step]);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return { count, increment, decrement, reset };
}
// フォーム用のカスタムフック
function useForm(initialState: T) {
const [state, setState] = useState(initialState);
const updateField = useCallback((
) => {
setState(prev => ({
...prev,
[field]: value
}));
}, []);
const reset = useCallback(() => {
setState(initialState);
}, [initialState]);
return { state, updateField, reset };
}
// 使用例
const CounterApp: React.FC = () => {
const { count, increment, decrement, reset } = useCounter(0, 2);
return (
Count: {count}
+2 -2 Reset
); };
まとめ
useStateフックとTypeScriptを組み合わせることで、型安全で保守しやすい状態管理を実現できます。シンプルなカウンターから複雑なフォームまで、様々なパターンを理解することで、実際のアプリケーション開発に活用できます。
重要なポイント:
TypeScriptの型推論と明示的な型定義の使い分け
オブジェクトや配列の不変性を保った更新方法
パフォーマンス最適化のためのメモ化技術
カスタムフックによるロジックの再利用
次のステップとして、イベントハンドラーの実装やより高度な状態管理パターンについて学習することで、さらに実用的なReactアプリケーションを構築できるようになります。