React Hooksは、2018年にReact 16.8で導入されて以来、Reactアプリケーション開発のスタンダードとなっています。関数コンポーネントでstate管理やライフサイクル機能を使用できるようになり、コードの可読性と再利用性が大幅に向上しました。本記事では、React Hooksの基本から実践的な活用法まで、現役エンジニアが知るべき知識を体系的に解説します。
React Hooksの基本概念
Hooksとは何か
React Hooksは、関数コンポーネント内でReactの機能(stateやライフサイクルメソッドなど)を「フック」して使用するための仕組みです。クラスコンポーネントでしか使用できなかった機能を、関数コンポーネントでも利用可能にする革新的な機能です。
Hooksの名前は必ず「use」で始まる規則があり、これによりReactがHooksを識別し、適切に動作させています。例えば、useState
、useEffect
、useContext
などがあります。
Hooksのメリット
従来のクラスコンポーネントと比較して、Hooksには以下のメリットがあります:
- コードの簡潔性: クラス構文が不要で、より短いコードで同等の機能を実装可能
- ロジックの再利用: カスタムHooksにより、コンポーネント間でのロジック共有が容易
- テストの容易さ: 関数として独立したロジックのテストが可能
- バンドルサイズの削減: 関数コンポーネントはクラスコンポーネントより軽量
基本的なHooks
useState:状態管理の基礎
useState
は最も基本的なHookで、関数コンポーネント内でローカル状態を管理します。
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>現在のカウント: {count}</p>
<button onClick={() => setCount(count + 1)}>
カウントアップ
</button>
</div>
);
}
useState
は配列を返し、第一要素が現在の状態値、第二要素が状態更新関数となります。初期値は引数として渡します。
useEffect:副作用の処理
useEffect
は、コンポーネントのライフサイクルに関連する副作用(API呼び出し、DOM操作、タイマーなど)を処理するHookです。
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('ユーザー取得エラー:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // userIdが変更された時のみ実行
if (loading) return <div>読み込み中...</div>;
return <div>ユーザー名: {user?.name}</div>;
}
第二引数の依存配列により、いつeffectを実行するかを制御できます。
useContext:グローバル状態管理
useContext
は、Reactのコンテキスト機能をHooksで利用するためのものです。プロップドリリングを避け、コンポーネント階層を跨いだデータ共有が可能になります。
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
</ThemeContext.Provider>
);
}
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<header className={`header-${theme}`}>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
テーマ切替
</button>
</header>
);
}
パフォーマンス最適化のHooks
useMemo:計算結果のメモ化
useMemo
は重い計算処理の結果をメモ化し、依存値が変更されない限り再計算を防ぎます。
import React, { useState, useMemo } from 'react';
function ExpensiveComponent({ items, filter }) {
const [sortOrder, setSortOrder] = useState('asc');
const processedItems = useMemo(() => {
console.log('重い処理を実行中...');
return items
.filter(item => item.name.includes(filter))
.sort((a, b) => {
if (sortOrder === 'asc') {
return a.name.localeCompare(b.name);
} else {
return b.name.localeCompare(a.name);
}
});
}, [items, filter, sortOrder]);
return (
<div>
<button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
ソート順変更
</button>
<ul>
{processedItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
useCallback:関数のメモ化
useCallback
は関数をメモ化し、依存値が変更されない限り同じ関数インスタンスを返します。子コンポーネントへの不要な再レンダリングを防ぐのに有効です。
import React, { useState, useCallback } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
const addTodo = useCallback(() => {
if (newTodo.trim()) {
setTodos(prev => [...prev, { id: Date.now(), text: newTodo }]);
setNewTodo('');
}
}, [newTodo]);
const removeTodo = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<div>
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="新しいTodo"
/>
<button onClick={addTodo}>追加</button>
<TodoList todos={todos} onRemove={removeTodo} />
</div>
);
}
カスタムHooks
カスタムHooksの基本
カスタムHooksは、複数のコンポーネント間でロジックを共有するための仕組みです。「use」で始まる関数として定義し、内部で他のHooksを使用できます。
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('localStorage読み取りエラー:', error);
return initialValue;
}
});
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('localStorage書き込みエラー:', error);
}
};
return [storedValue, setValue];
}
// 使用例
function Settings() {
const [settings, setSettings] = useLocalStorage('appSettings', {
theme: 'light',
language: 'ja'
});
return (
<div>
<label>
テーマ:
<select
value={settings.theme}
onChange={(e) => setSettings({...settings, theme: e.target.value})}
>
<option value="light">ライト</option>
<option value="dark">ダーク</option>
</select>
</label>
</div>
);
}
実践的なカスタムHooks例
API呼び出しを抽象化したカスタムHookの例:
import { useState, useEffect } from 'react';
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (url) {
fetchData();
}
}, [url]);
return { data, loading, error };
}
// 使用例
function UserList() {
const { data: users, loading, error } = useApi('/api/users');
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
実践的な活用パターン
フォーム管理
Hooksを使用したフォーム管理の実装例:
import React, { useState } from 'react';
function useForm(initialValues, validationRules = {}) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const setValue = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
// バリデーション実行
if (validationRules[name]) {
const error = validationRules[name](value);
setErrors(prev => ({ ...prev, [name]: error }));
}
};
const setTouched = (name) => {
setTouched(prev => ({ ...prev, [name]: true }));
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
};
return {
values,
errors,
touched,
setValue,
setTouched,
reset
};
}
function ContactForm() {
const validationRules = {
email: (value) => {
if (!value) return 'メールアドレスは必須です';
if (!/\S+@\S+\.\S+/.test(value)) return '有効なメールアドレスを入力してください';
return null;
},
name: (value) => !value ? '名前は必須です' : null
};
const { values, errors, touched, setValue, setTouched, reset } = useForm(
{ name: '', email: '', message: '' },
validationRules
);
const handleSubmit = (e) => {
e.preventDefault();
console.log('送信データ:', values);
reset();
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
placeholder="名前"
value={values.name}
onChange={(e) => setValue('name', e.target.value)}
onBlur={() => setTouched('name')}
/>
{touched.name && errors.name && (
<span className="error">{errors.name}</span>
)}
</div>
<div>
<input
type="email"
placeholder="メールアドレス"
value={values.email}
onChange={(e) => setValue('email', e.target.value)}
onBlur={() => setTouched('email')}
/>
{touched.email && errors.email && (
<span className="error">{errors.email}</span>
)}
</div>
<div>
<textarea
placeholder="メッセージ"
value={values.message}
onChange={(e) => setValue('message', e.target.value)}
/>
</div>
<button type="submit">送信</button>
</form>
);
}
状態管理パターン
複雑な状態管理にはuseReducerを活用:
import React, { useReducer } from 'react';
const initialState = {
cart: [],
total: 0,
isOpen: false
};
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
const existingItem = state.cart.find(item => item.id === action.payload.id);
if (existingItem) {
return {
...state,
cart: state.cart.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
),
total: state.total + action.payload.price
};
} else {
return {
...state,
cart: [...state.cart, { ...action.payload, quantity: 1 }],
total: state.total + action.payload.price
};
}
case 'REMOVE_ITEM':
const itemToRemove = state.cart.find(item => item.id === action.payload);
return {
...state,
cart: state.cart.filter(item => item.id !== action.payload),
total: state.total - (itemToRemove.price * itemToRemove.quantity)
};
case 'TOGGLE_CART':
return {
...state,
isOpen: !state.isOpen
};
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (id) => {
dispatch({ type: 'REMOVE_ITEM', payload: id });
};
const toggleCart = () => {
dispatch({ type: 'TOGGLE_CART' });
};
return (
<div>
<button onClick={toggleCart}>
カート ({state.cart.length}) - ¥{state.total}
</button>
{state.isOpen && (
<div className="cart-dropdown">
{state.cart.map(item => (
<div key={item.id} className="cart-item">
<span>{item.name} x {item.quantity}</span>
<button onClick={() => removeItem(item.id)}>削除</button>
</div>
))}
</div>
)}
</div>
);
}
React Hooksは、モダンなReact開発において必須のスキルです。基本的なHooksから始めて、カスタムHooksやパフォーマンス最適化テクニックまで段階的に習得することで、より効率的で保守性の高いReactアプリケーションを構築できるようになります。実際のプロジェクトでHooksを積極的に活用し、継続的な学習を通じてスキルを向上させていきましょう。