React Hooks完全入門:カスタムフックの作り方と実践パターン
React Hooksは、関数コンポーネントで状態管理やライフサイクル機能を使えるようにする革新的な機能です。この記事では、基本的なHooksから始めて、最終的には実用的なカスタムフックを作成できるようになることを目指します。
クラスコンポーネントの経験がなくても理解できるよう、段階的に解説していきます。
1. React Hooksとは
Hooksは、React 16.8で導入された機能で、関数コンポーネントに状態やライフサイクルのような機能を追加できるようにします。
1.1 Hooksを使う理由
- コードの簡潔性: クラスコンポーネントよりも短く、読みやすいコードが書ける
- ロジックの再利用性: カスタムフックを通じてロジックを共有しやすい
- 学習コストの低減: thisのバインディングなどの複雑な概念が不要
- テストの容易さ: 純粋な関数として扱えるため、テストが書きやすい
1.2 環境の準備
# 新しいReactプロジェクトの作成
npx create-react-app react-hooks-tutorial
cd react-hooks-tutorial
# または、既存のNext.jsプロジェクトで使用する場合
# Next.jsでは既にReact Hooksが利用可能です
2. 基本Hooks:useState
useStateは、コンポーネントに状態を追加するための最も基本的なHookです。
2.1 基本的な使い方
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>
インクリメント
</button>
<button onClick={() => setCount(count - 1)}>
デクリメント
</button>
</div>
);
}
2.2 複数の状態の管理
function UserForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log({ name, email, age });
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="名前"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="メールアドレス"
/>
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
placeholder="年齢"
/>
<button type="submit">送信</button>
</form>
);
}
2.3 オブジェクトや配列の状態管理
function TodoList() {
const [todos, setTodos] = useState<Array<{ id: number; text: string; done: boolean }>>([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, {
id: Date.now(),
text: input,
done: false
}]);
setInput('');
}
};
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="新しいTodo"
/>
<button onClick={addTodo}>追加</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
</li>
))}
</ul>
</div>
);
}
3. 基本Hooks:useEffect
useEffectは、副作用(side effects)を処理するためのHookです。データの取得、購読の設定、DOMの操作などに使用します。
3.1 基本的な使い方
import { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// コンポーネントのマウント時と依存配列の値が変わった時に実行
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []); // 空の配列 = マウント時のみ実行
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error}</div>;
return <div>{JSON.stringify(data)}</div>;
}
3.2 依存配列の使い方
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState(null);
useEffect(() => {
// userIdが変わった時に実行
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(setUser);
}, [userId]); // userIdが変わった時に再実行
return <div>{user?.name}</div>;
}
3.3 クリーンアップ関数
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// クリーンアップ関数(アンマウント時や依存配列の値が変わった時に実行)
return () => clearInterval(interval);
}, []);
return <div>経過時間: {seconds}秒</div>;
}
3.4 イベントリスナーの設定とクリーンアップ
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div>
ウィンドウサイズ: {size.width} x {size.height}
</div>
);
}
4. 基本Hooks:useContext
useContextは、コンポーネントツリー全体でデータを共有するためのHookです。
4.1 Contextの作成と使用
import { createContext, useContext, useState, ReactNode } from 'react';
// Contextの作成
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Providerコンポーネント
function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// カスタムフック(安全にContextを使用するためのヘルパー)
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// 使用例
function App() {
return (
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
);
}
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'dark' ? '#333' : '#fff',
color: theme === 'dark' ? '#fff' : '#333'
}}
>
テーマを切り替え(現在: {theme})
</button>
);
}
5. 基本Hooks:useReducer
useReducerは、複雑な状態ロジックを管理するためのHookで、useStateの代替として使用できます。
5.1 基本的な使い方
import { useReducer } from 'react';
// アクションの型定義
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' }
| { type: 'set'; payload: number };
// Reducer関数
function counterReducer(state: number, action: Action): number {
switch (action.type) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
case 'reset':
return 0;
case 'set':
return action.payload;
default:
return state;
}
}
function Counter() {
const [count, dispatch] = useReducer(counterReducer, 0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>リセット</button>
<button onClick={() => dispatch({ type: 'set', payload: 10 })}>
10に設定
</button>
</div>
);
}
5.2 複雑な状態の管理
interface Todo {
id: number;
text: string;
done: boolean;
}
type TodoAction =
| { type: 'add'; payload: string }
| { type: 'toggle'; payload: number }
| { type: 'delete'; payload: number }
| { type: 'clear' };
function todoReducer(state: Todo[], action: TodoAction): Todo[] {
switch (action.type) {
case 'add':
return [...state, {
id: Date.now(),
text: action.payload,
done: false
}];
case 'toggle':
return state.map(todo =>
todo.id === action.payload
? { ...todo, done: !todo.done }
: todo
);
case 'delete':
return state.filter(todo => todo.id !== action.payload);
case 'clear':
return [];
default:
return state;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
const [input, setInput] = useState('');
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter' && input.trim()) {
dispatch({ type: 'add', payload: input });
setInput('');
}
}}
/>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => dispatch({ type: 'toggle', payload: todo.id })}
/>
<span>{todo.text}</span>
<button onClick={() => dispatch({ type: 'delete', payload: todo.id })}>
削除
</button>
</li>
))}
</ul>
</div>
);
}
6. 便利なHooks:useMemoとuseCallback
6.1 useMemo(計算結果のメモ化)
import { useState, useMemo } from 'react';
function ExpensiveCalculation({ numbers }: { numbers: number[] }) {
// 重い計算をメモ化
const sum = useMemo(() => {
console.log('計算中...');
return numbers.reduce((acc, num) => acc + num, 0);
}, [numbers]); // numbersが変わった時のみ再計算
return <div>合計: {sum}</div>;
}
6.2 useCallback(関数のメモ化)
import { useState, useCallback, memo } from 'react';
const ExpensiveChild = memo(({ onClick, name }: { onClick: () => void; name: string }) => {
console.log(`${name}が再レンダリングされました`);
return <button onClick={onClick}>{name}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// 関数をメモ化することで、不要な再レンダリングを防ぐ
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []); // 依存配列が空なので、関数は一度だけ作成される
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<ExpensiveChild onClick={handleClick} name="ボタン" />
<p>カウント: {count}</p>
</div>
);
}
7. カスタムフックの作成
カスタムフックは、複数のコンポーネントで再利用可能なロジックを抽出するための仕組みです。
7.1 データフェッチングのカスタムフック
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
// 使用例
function UserProfile({ userId }: { userId: number }) {
const { data, loading, error } = useFetch<User>(`/api/users/${userId}`);
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error}</div>;
return <div>{data?.name}</div>;
}
7.2 ローカルストレージのカスタムフック
function useLocalStorage<T>(key: string, initialValue: T) {
// ローカルストレージから初期値を読み取る
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// 値を更新し、ローカルストレージにも保存する関数
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
// 使用例
function Settings() {
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
return (
<button onClick={() => setTheme(prev => prev === 'light' ? 'dark' : 'light')}>
テーマ: {theme}
</button>
);
}
7.3 デバウンスのカスタムフック
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// 使用例:検索機能
function SearchBox() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
// API呼び出しなど
console.log('検索:', debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="検索..."
/>
);
}
7.4 ウィンドウサイズのカスタムフック
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0,
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener('resize', handleResize);
handleResize(); // 初期値を設定
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}
// 使用例
function ResponsiveLayout() {
const { width } = useWindowSize();
const isMobile = width < 768;
return (
<div>
{isMobile ? <MobileMenu /> : <DesktopMenu />}
</div>
);
}
8. 実践的なカスタムフック:API管理
8.1 認証状態の管理
interface User {
id: number;
name: string;
email: string;
}
function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// トークンからユーザー情報を取得
const token = localStorage.getItem('token');
if (token) {
fetch('/api/me', {
headers: { Authorization: `Bearer ${token}` }
})
.then(res => res.json())
.then(setUser)
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
const login = async (email: string, password: string) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const { token, user } = await response.json();
localStorage.setItem('token', token);
setUser(user);
};
const logout = () => {
localStorage.removeItem('token');
setUser(null);
};
return { user, loading, login, logout };
}
9. Hooksのベストプラクティス
9.1 Hooksのルール
- 最上位でのみ呼び出す: ループや条件分岐の中で呼び出さない
- React関数からのみ呼び出す: 通常のJavaScript関数から呼び出さない
// ❌ 悪い例
function BadComponent() {
if (condition) {
const [state, setState] = useState(0); // エラー!
}
}
// ✅ 良い例
function GoodComponent() {
const [state, setState] = useState(0);
if (condition) {
// stateを使用
}
}
9.2 パフォーマンスの最適化
// 不要な再レンダリングを防ぐ
const MemoizedComponent = memo(ExpensiveComponent);
// 依存配列を適切に設定
useEffect(() => {
// 処理
}, [dependency1, dependency2]); // 必要な依存関係のみを指定
10. まとめと次のステップ
この記事を通じて、React Hooksの基礎から実践的なカスタムフックの作成まで学びました。
学んだこと
- 基本Hooks: useState, useEffect, useContext, useReducer
- 最適化Hooks: useMemo, useCallback
- カスタムフック: ロジックの再利用と抽象化
- ベストプラクティス: Hooksのルールとパフォーマンス最適化
次のステップ
- より高度なHooks: useRef, useImperativeHandle, useLayoutEffect
- 外部ライブラリ: react-query(データフェッチング)、zustand(状態管理)
- テスト: React Testing Libraryを使ったHooksのテスト
- 実践プロジェクト: 実際のアプリケーションでのHooks活用
React Hooksは、モダンなReact開発の基盤となる重要な機能です。継続的な学習と実践を通じて、効率的で保守しやすいコードを書けるようになりましょう!
Happy Hooking!
コメント
コメントを読み込み中...
