このチュートリアルで学ぶこと
✓ useState - 状態管理の基本
✓ useEffect - 副作用の処理
✓ useContext - グローバル状態の共有
✓ useReducer - 複雑な状態管理
✓ useMemo/useCallback - パフォーマンス最適化
✓ カスタムフック - ロジックの再利用
前提条件
- JavaScriptの基本知識
- Reactの基本的な理解(コンポーネント、props)
- Node.jsがインストールされていること
プロジェクトのセットアップ
# プロジェクト作成
npx create-react-app hooks-tutorial
cd hooks-tutorial
# 開発サーバー起動
npm start
Step 1: useState - 状態管理の基本
基本的な使い方
// src/components/Counter.jsx
import { useState } from 'react';
export default function Counter() {
// [状態値, 更新関数] = useState(初期値)
const [count, setCount] = useState(0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>
増加
</button>
<button onClick={() => setCount(count - 1)}>
減少
</button>
<button onClick={() => setCount(0)}>
リセット
</button>
</div>
);
}
オブジェクトの状態管理
// src/components/UserForm.jsx
import { useState } from 'react';
export default function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
// スプレッド演算子で既存の値を保持しつつ更新
setUser(prev => ({
...prev,
[name]: value
}));
};
return (
<form>
<input
name="name"
value={user.name}
onChange={handleChange}
placeholder="名前"
/>
<input
name="email"
value={user.email}
onChange={handleChange}
placeholder="メール"
/>
<input
name="age"
type="number"
value={user.age}
onChange={handleChange}
placeholder="年齢"
/>
<pre>{JSON.stringify(user, null, 2)}</pre>
</form>
);
}
配列の状態管理
// src/components/TodoList.jsx
import { useState } from 'react';
export default function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (!input.trim()) return;
setTodos([...todos, { id: Date.now(), text: input, done: false }]);
setInput('');
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>追加</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>削除</button>
</li>
))}
</ul>
</div>
);
}
Step 2: useEffect - 副作用の処理
基本的なuseEffect
// src/components/Timer.jsx
import { useState, useEffect } from 'react';
export default function Timer() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
let interval = null;
if (isRunning) {
interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
}
// クリーンアップ関数
return () => {
if (interval) clearInterval(interval);
};
}, [isRunning]); // isRunningが変化したときに実行
return (
<div>
<p>経過時間: {seconds}秒</p>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? '停止' : '開始'}
</button>
<button onClick={() => setSeconds(0)}>リセット</button>
</div>
);
}
データフェッチング
// src/components/UserList.jsx
import { useState, useEffect } from 'react';
export default function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const response = await fetch(
'https://jsonplaceholder.typicode.com/users'
);
if (!response.ok) throw new Error('データの取得に失敗しました');
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []); // 空の依存配列 = マウント時のみ実行
if (loading) return <p>読み込み中...</p>;
if (error) return <p>エラー: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
);
}
ローカルストレージとの同期
// src/components/PersistentCounter.jsx
import { useState, useEffect } from 'react';
export default function PersistentCounter() {
const [count, setCount] = useState(() => {
// 遅延初期化
const saved = localStorage.getItem('count');
return saved ? JSON.parse(saved) : 0;
});
useEffect(() => {
localStorage.setItem('count', JSON.stringify(count));
}, [count]);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>増加</button>
</div>
);
}
Step 3: useContext - グローバル状態の共有
Contextの作成と使用
// src/context/ThemeContext.jsx
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// src/components/ThemedApp.jsx
import { useTheme } from '../context/ThemeContext';
export default function ThemedApp() {
const { theme, toggleTheme } = useTheme();
const styles = {
container: {
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
padding: '20px',
minHeight: '100vh'
}
};
return (
<div style={styles.container}>
<h1>現在のテーマ: {theme}</h1>
<button onClick={toggleTheme}>テーマ切替</button>
</div>
);
}
// src/App.jsx
import { ThemeProvider } from './context/ThemeContext';
import ThemedApp from './components/ThemedApp';
export default function App() {
return (
<ThemeProvider>
<ThemedApp />
</ThemeProvider>
);
}
Step 4: useReducer - 複雑な状態管理
Reducerパターンの実装
// src/components/ShoppingCart.jsx
import { useReducer } from 'react';
const initialState = {
items: [],
total: 0
};
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find(
item => item.id === action.payload.id
);
if (existingItem) {
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
),
total: state.total + action.payload.price
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
total: state.total + action.payload.price
};
}
case 'REMOVE_ITEM': {
const item = state.items.find(i => i.id === action.payload);
return {
...state,
items: state.items.filter(i => i.id !== action.payload),
total: state.total - (item.price * item.quantity)
};
}
case 'CLEAR_CART':
return initialState;
default:
return state;
}
}
const products = [
{ id: 1, name: 'りんご', price: 100 },
{ id: 2, name: 'バナナ', price: 80 },
{ id: 3, name: 'オレンジ', price: 120 }
];
export default function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
return (
<div>
<h2>商品一覧</h2>
{products.map(product => (
<div key={product.id}>
{product.name} - ¥{product.price}
<button onClick={() => dispatch({
type: 'ADD_ITEM',
payload: product
})}>
カートに追加
</button>
</div>
))}
<h2>カート</h2>
{state.items.map(item => (
<div key={item.id}>
{item.name} x {item.quantity} = ¥{item.price * item.quantity}
<button onClick={() => dispatch({
type: 'REMOVE_ITEM',
payload: item.id
})}>
削除
</button>
</div>
))}
<p>合計: ¥{state.total}</p>
<button onClick={() => dispatch({ type: 'CLEAR_CART' })}>
カートを空にする
</button>
</div>
);
}
Step 5: useMemo / useCallback - パフォーマンス最適化
useMemo - 計算結果のメモ化
// src/components/ExpensiveCalculation.jsx
import { useState, useMemo } from 'react';
function slowFunction(num) {
console.log('計算中...');
for (let i = 0; i < 1000000000; i++) {} // 重い処理
return num * 2;
}
export default function ExpensiveCalculation() {
const [number, setNumber] = useState(0);
const [dark, setDark] = useState(false);
// numberが変わったときだけ再計算
const doubledNumber = useMemo(() => {
return slowFunction(number);
}, [number]);
const theme = {
backgroundColor: dark ? '#333' : '#fff',
color: dark ? '#fff' : '#333'
};
return (
<div style={theme}>
<input
type="number"
value={number}
onChange={(e) => setNumber(parseInt(e.target.value) || 0)}
/>
<p>結果: {doubledNumber}</p>
<button onClick={() => setDark(!dark)}>
テーマ切替
</button>
</div>
);
}
useCallback - 関数のメモ化
// src/components/CallbackExample.jsx
import { useState, useCallback, memo } from 'react';
// memo化された子コンポーネント
const SearchButton = memo(({ onClick }) => {
console.log('SearchButtonがレンダリングされました');
return <button onClick={onClick}>検索</button>;
});
export default function CallbackExample() {
const [query, setQuery] = useState('');
const [count, setCount] = useState(0);
// queryが変わったときだけ新しい関数を作成
const handleSearch = useCallback(() => {
console.log(`検索: ${query}`);
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="検索ワード"
/>
<SearchButton onClick={handleSearch} />
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>
カウント増加(これでSearchButtonは再レンダリングされない)
</button>
</div>
);
}
Step 6: カスタムフック - ロジックの再利用
useLocalStorage
// src/hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
export function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
useFetch
// src/hooks/useFetch.js
import { useState, useEffect } from 'react';
export function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, {
signal: abortController.signal
});
if (!response.ok) throw new Error('Network error');
const json = await response.json();
setData(json);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => abortController.abort();
}, [url]);
return { data, loading, error };
}
useDebounce
// src/hooks/useDebounce.js
import { useState, useEffect } from 'react';
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
カスタムフックの使用例
// src/components/SearchWithDebounce.jsx
import { useState } from 'react';
import { useDebounce } from '../hooks/useDebounce';
import { useFetch } from '../hooks/useFetch';
export default function SearchWithDebounce() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 500);
const { data, loading, error } = useFetch(
debouncedSearch
? `https://api.github.com/search/users?q=${debouncedSearch}`
: null
);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="GitHubユーザーを検索..."
/>
{loading && <p>検索中...</p>}
{error && <p>エラー: {error}</p>}
{data?.items?.map(user => (
<div key={user.id}>
<img src={user.avatar_url} alt="" width={32} />
{user.login}
</div>
))}
</div>
);
}
ベストプラクティス
1. useStateの注意点
- 状態は必要最小限に
- 派生値はuseStateではなく計算で
- オブジェクトの更新時はスプレッド演算子で
2. useEffectの注意点
- 依存配列を正確に設定
- クリーンアップ関数を忘れずに
- 無限ループに注意
3. パフォーマンス最適化
- 過度な最適化は避ける
- まず計測してから最適化
- memo, useMemo, useCallbackは必要な場所のみ
4. カスタムフック
- ロジックの再利用に活用
- 名前は「use」で始める
- 単一責任の原則を守る
まとめ
React Hooksを使うことで、関数コンポーネントで状態管理や副作用を扱えるようになります。基本のuseState、useEffectをマスターした後、useContext、useReducerで複雑な状態管理に挑戦し、useMemo、useCallbackでパフォーマンスを最適化しましょう。
← 一覧に戻る