目次
現代のReact開発を語る上で、React Hooks(フック)の理解は絶対に欠かせません。2019年にReact 16.8で正式に導入されて以来、フックはReactコンポーネントの書き方を根本から変え、よりシンプルで直感的な開発を可能にしました。本記事では、「フックって一体何?」という基本から、主要なフックの使い方、ロジックを再利用するためのカスタムフック、そしてパフォーマンスを意識したベストプラクティスまで、React Hooksの全体像を網羅的に解説します。
React Hooksが切り拓いた新しい開発パラダイム
クラスコンポーネントからの解放:なぜフックが生まれたのか
React Hooksの登場は、単なる新機能の追加ではなく、React開発の根本的なパラダイムシフトでした。フックが解決しようとした問題を理解することで、その真の価値が見えてきます。
フック登場以前のReact開発では、stateやライフサイクルメソッドを使用するためには、必ずクラスコンポーネントを書く必要がありました。典型的なクラスコンポーネントは以下のような形でした:
javascript
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
// thisバインディングが必要
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<button onClick={this.handleClick}>
Count: {this.state.count}
</button>
);
}
}
このコードには、いくつかの本質的な課題がありました。
thisバインディングの複雑さは、多くの開発者を悩ませました。JavaScriptのthisは、呼び出し方によって参照先が変わる特殊な挙動を持ちます。上記の例では、constructorで明示的にbindするか、アロー関数を使う必要があり、これは初心者にとって大きな障壁となっていました。
ロジックの分散問題も深刻でした。例えば、データの取得とクリーンアップという関連する処理が、異なるライフサイクルメソッドに分かれてしまいます:
javascript
class UserProfile extends React.Component {
componentDidMount() {
// データ取得の開始
this.fetchUserData(this.props.userId);
// イベントリスナーの登録
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
// クリーンアップ処理が別の場所に
window.removeEventListener('resize', this.handleResize);
}
// 関連する処理が分散してしまう
}
React Hooksは、これらすべての問題を優雅に解決しました。同じカウンターをフックで書くと:
javascript
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
コードは劇的にシンプルになり、thisの問題も完全に解消されました。
関数コンポーネントの完全な勝利
フックの導入により、関数コンポーネントはクラスコンポーネントのすべての機能を獲得しました。それどころか、多くの面でクラスコンポーネントを上回る利点を持つようになりました。
コードの簡潔性は最も顕著な利点です。同じ機能を実装するのに必要なコード量を比較してみましょう:
javascript
// クラスコンポーネント(約20行)
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = { seconds: 0 };
}
componentDidMount() {
this.interval = setInterval(() => {
this.setState({ seconds: this.state.seconds + 1 });
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return <div>Seconds: {this.state.seconds}</div>;
}
}
// 関数コンポーネント with Hooks(約10行)
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>Seconds: {seconds}</div>;
}
関数コンポーネントの方が約半分のコード量で、より読みやすく実装できています。
フックのルール:なぜ制約があるのか
React Hooksには、使用する際に守らなければならない2つの重要なルールがあります。
ルール1:トップレベルでのみ呼び出す
javascript
// ❌ 悪い例:条件文の中でフックを呼び出している
function BadComponent({ shouldTrack }) {
if (shouldTrack) {
useEffect(() => {
console.log('Tracking...');
}, []);
}
}
// ✅ 良い例:フックは常にトップレベルで呼び出す
function GoodComponent({ shouldTrack }) {
useEffect(() => {
if (shouldTrack) {
console.log('Tracking...');
}
}, [shouldTrack]);
}
このルールが存在する理由は、Reactがフックの呼び出し順序に依存してstateを管理しているからです。条件によってフックの呼び出しがスキップされると、Reactは各フックがどのstateに対応するかを正しく追跡できなくなります。
基本フック3つ:React開発の土台
useState:状態管理の基礎
useState
は、最も基本的で最も頻繁に使用されるフックです。その使い方を詳しく見ていきましょう。
基本的な使い方:
javascript
function LoginForm() {
// 文字列の状態
const [username, setUsername] = useState('');
// 真偽値の状態
const [isLoading, setIsLoading] = useState(false);
// オブジェクトの状態
const [formData, setFormData] = useState({
email: '',
password: ''
});
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
await loginAPI({ username, ...formData });
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={isLoading}
/>
{/* 他のフィールド */}
</form>
);
}
関数型更新の重要性:
javascript
function Counter() {
const [count, setCount] = useState(0);
const handleMultipleIncrements = () => {
// ❌ 悪い例:古い値を参照してしまう可能性
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// 結果:1しか増えない
// ✅ 良い例:関数型更新で常に最新の値を参照
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
// 結果:3増える
};
}
遅延初期化によるパフォーマンス最適化:
javascript
function ExpensiveComponent() {
// ❌ 悪い例:毎回のレンダリングで高コストな計算が実行される
const [data, setData] = useState(calculateExpensiveInitialData());
// ✅ 良い例:初回レンダリング時のみ実行される
const [data, setData] = useState(() => calculateExpensiveInitialData());
}
useEffect:副作用の管理
useEffect
は、Reactコンポーネントにおける副作用を管理するための中心的なフックです。
基本的なデータフェッチング:
javascript
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 非同期処理を扱う方法
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // userIdが変更されたときに再実行
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>Welcome, {user?.name}!</div>;
}
クリーンアップ関数の重要性:
javascript
function MouseTracker() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
// イベントリスナーの登録
window.addEventListener('mousemove', handleMouseMove);
// クリーンアップ関数で後片付け
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []); // 空の依存配列 = マウント時のみ実行
return (
<div>
Mouse position: {position.x}, {position.y}
</div>
);
}
useRef:参照の保持とDOM操作
useRef
は、再レンダリングをトリガーせずに値を保持したり、DOM要素への参照を保持したりするためのフックです。
DOM要素への参照:
javascript
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// コンポーネントマウント時に自動フォーカス
inputRef.current?.focus();
}, []);
return (
<input
ref={inputRef}
placeholder="I will be focused automatically"
/>
);
}
値の保持(前回の値を記憶):
javascript
function ValueChangeDetector({ value }) {
const previousValue = useRef();
useEffect(() => {
if (previousValue.current !== undefined && previousValue.current !== value) {
console.log(`Value changed from ${previousValue.current} to ${value}`);
}
previousValue.current = value;
});
return <div>Current value: {value}</div>;
}
高度な標準フック:より洗練された状態管理
useContext:グローバル状態の共有
useContext
を使用すると、深いコンポーネントツリーでもprops drillingなしにデータを共有できます。
javascript
// テーマコンテキストの作成
const ThemeContext = React.createContext();
// プロバイダーコンポーネント
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>
);
}
// 深くネストしたコンポーネントでの使用
function DeepChildComponent() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'dark' ? '#333' : '#fff',
color: theme === 'dark' ? '#fff' : '#333'
}}
>
Current theme: {theme}
</button>
);
}
useReducer:複雑な状態遷移の管理
useReducer
は、複雑な状態ロジックを管理するのに適しています。
javascript
// フォームの状態管理の例
const initialState = {
username: '',
email: '',
password: '',
errors: {},
isSubmitting: false
};
function formReducer(state, action) {
switch (action.type) {
case 'UPDATE_FIELD':
return {
...state,
[action.field]: action.value,
errors: {
...state.errors,
[action.field]: '' // フィールド更新時にエラーをクリア
}
};
case 'SET_ERRORS':
return { ...state, errors: action.errors };
case 'SUBMIT_START':
return { ...state, isSubmitting: true };
case 'SUBMIT_SUCCESS':
return { ...initialState }; // 成功時はリセット
case 'SUBMIT_ERROR':
return { ...state, isSubmitting: false };
default:
return state;
}
}
function SignUpForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleChange = (e) => {
dispatch({
type: 'UPDATE_FIELD',
field: e.target.name,
value: e.target.value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: 'SUBMIT_START' });
try {
await submitForm(state);
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (error) {
dispatch({ type: 'SUBMIT_ERROR' });
dispatch({ type: 'SET_ERRORS', errors: error.validationErrors });
}
};
return (
<form onSubmit={handleSubmit}>
<input
name="username"
value={state.username}
onChange={handleChange}
disabled={state.isSubmitting}
/>
{state.errors.username && (
<span className="error">{state.errors.username}</span>
)}
{/* 他のフィールド */}
</form>
);
}
useMemoとuseCallback:パフォーマンス最適化
これらのフックは、不要な再計算や再生成を防ぐために使用します。
javascript
function ExpensiveList({ items, filter }) {
// 高コストなフィルタリング処理をメモ化
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]); // これらが変更されたときのみ再計算
// 子コンポーネントに渡す関数をメモ化
const handleItemClick = useCallback((id) => {
console.log(`Item ${id} clicked`);
}, []); // 依存配列が空 = 関数は一度だけ作成される
return (
<div>
{filteredItems.map(item => (
<MemoizedItem
key={item.id}
item={item}
onClick={handleItemClick}
/>
))}
</div>
);
}
// React.memoでラップされた子コンポーネント
const MemoizedItem = React.memo(({ item, onClick }) => {
console.log(`Rendering item ${item.id}`);
return (
<div onClick={() => onClick(item.id)}>
{item.name}
</div>
);
});
カスタムフック:ロジック再利用の芸術
実用的なカスタムフックの例
カスタムフックは、複雑なロジックを再利用可能な形で抽出する強力な手段です。
useLocalStorage – ローカルストレージと同期するstate:
javascript
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(`Error loading localStorage key "${key}":`, error);
return initialValue;
}
});
// 値の更新
const setValue = useCallback((value) => {
try {
// 関数型更新もサポート
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error saving localStorage key "${key}":`, error);
}
}, [key, storedValue]);
return [storedValue, setValue];
}
// 使用例
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);
return (
<div>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle theme: {theme}
</button>
<input
type="range"
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
min="12"
max="24"
/>
</div>
);
}
useFetch – データフェッチングの抽象化:
javascript
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!cancelled) {
setData(data);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchData();
// クリーンアップで実行中のリクエストをキャンセル
return () => {
cancelled = true;
};
}, [url]); // URLが変更されたら再フェッチ
return { data, loading, error };
}
// 使用例
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
useDebounce – 値の変更を遅延させる:
javascript
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// 使用例:検索フィールド
function SearchUsers() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const { data: results } = useFetch(
debouncedSearchTerm ? `/api/users/search?q=${debouncedSearchTerm}` : null
);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search users..."
/>
{results && (
<ul>
{results.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
}
ベストプラクティスとよくある落とし穴
依存配列の正しい理解と管理
依存配列は、React Hooksを使いこなす上で最も重要で、最も誤解されやすい概念です。
javascript
function SearchResults({ query, filters }) {
const [results, setResults] = useState([]);
// ❌ 悪い例:必要な依存を含めていない
useEffect(() => {
searchAPI(query, filters).then(setResults);
}, [query]); // filtersの変更が検知されない!
// ✅ 良い例:すべての依存を含める
useEffect(() => {
searchAPI(query, filters).then(setResults);
}, [query, filters]);
// ⚠️ 注意:オブジェクトの比較
// filtersがオブジェクトの場合、毎回新しい参照になる可能性がある
// その場合は、個別のプロパティを依存配列に含めるか、useMemoを使用
useEffect(() => {
searchAPI(query, filters).then(setResults);
}, [query, filters.category, filters.priceRange]); // より安定
}
パフォーマンスの罠と最適化戦略
React Hooksを使用する際のパフォーマンスに関する注意点:
javascript
function ProductList({ products }) {
// ❌ 悪い例:すべてをメモ化
const memoizedProducts = useMemo(() => products, [products]);
const handleClick = useCallback((id) => {
console.log(id);
}, []);
// ✅ 良い例:本当に必要な場合のみメモ化
// 高コストな計算の場合
const expensiveCalculation = useMemo(() => {
return products.reduce((acc, product) => {
// 複雑な計算...
return acc + complexCalculation(product);
}, 0);
}, [products]);
// 子コンポーネントが React.memo でラップされている場合
const handleAddToCart = useCallback((productId) => {
addToCart(productId);
}, [addToCart]);
return (
<div>
{products.map(product => (
<MemoizedProductCard
key={product.id}
product={product}
onAddToCart={handleAddToCart}
/>
))}
</div>
);
}
デバッグテクニック
React Hooksのデバッグに役立つテクニック:
javascript
// カスタムフックのデバッグ
function useCustomHook(value) {
const [state, setState] = useState(value);
// React DevTools で表示される
useDebugValue(state > 100 ? 'High' : 'Low');
// より詳細な情報を表示
useDebugValue(state, (state) => {
return `State: ${state}, Category: ${state > 100 ? 'High' : 'Low'}`;
});
return [state, setState];
}
// 依存配列の変更を追跡
function useWhyDidYouUpdate(name, props) {
const previousProps = useRef();
useEffect(() => {
if (previousProps.current) {
const allKeys = Object.keys({ ...previousProps.current, ...props });
const changedProps = {};
allKeys.forEach(key => {
if (previousProps.current[key] !== props[key]) {
changedProps[key] = {
from: previousProps.current[key],
to: props[key]
};
}
});
if (Object.keys(changedProps).length) {
console.log('[why-did-you-update]', name, changedProps);
}
}
previousProps.current = props;
});
}
まとめ:React Hooksがもたらした革命
React Hooksは、単なる新機能ではなく、React開発の根本的な変革でした。クラスコンポーネントの複雑さから解放され、より直感的で再利用可能なコードを書けるようになりました。
基本的なuseStateとuseEffectから始まり、高度なuseReducerやuseMemo、そしてカスタムフックの作成まで、段階的に学んでいくことで、React Hooksの真の力を引き出せます。
重要なのは、フックは道具であり、適切に使用してこそ価値を発揮するということです。すべての問題をフックで解決しようとするのではなく、適材適所で使い分けることが大切です。
この記事で紹介したコード例とベストプラクティスを参考に、ぜひ実際に手を動かしてReact Hooksの世界を探求してみてください。最初は戸惑うかもしれませんが、一度その力を理解すれば、もうクラスコンポーネントには戻れなくなるでしょう。