React Hooks は、クラスを書かずに関数コンポーネントでステート、イフェクト、コンテキスト、および React の他の機能を使用できるようにする関数です。React 16.8 で導入されたフックは、HOC や render props といった複雑なパターンを置き換え、開発アプローチを根本的に変えました。React, 2024によると、15つの組み込みフックがあり、すべてのコンポーネント管理シナリオをカバーします。
まとめ
React Hooks は、クラスを作らずにフレームワークの内部メカニズムに“フックする”ことができる関数のセットです。React 16.8 以前は、関数コンポーネントは props を受け取って JSX を返すことしかできませんでした — 複雑なロジックにはクラスコンポーネントが必要でした。
クラスコンポーネントの問題は関連するロジックの分割です。同じエンティティに関連するコード(例えば、WebSocket サブスクリプション)は、componentDidMount、componentDidUpdate、componentWillUnmount の間に強制的に分けられていました。フックはこれを解決します: すべてのロジックが1つの useEffect にまとめられます。
2つ目の問題はロジックの再利用です。クラスコンポーネント間で状態ロジックを再利用するには、HOC(高層コンポーネント)または render props を適用する必要がありました — どちらのパターンもコンポーネントツリーに余計なネストを作りました。カスタムフックはネストなしでこれを解決します。
useState は最も基本的なフックです。初期値を受け取り、タプルを返します: 現在の値とそれを更新する関数です。クラスの this.setState とは異なり、useState はオブジェクトをマージしません — 新しい状態は、特定の useState 呼び出しに対して前の状態を完全に置き換えます。
import React, { useState } from 'react';
function Form() {
const [text, setText] = useState('');
const [submitting, setSubmitting] = useState(false);
async function handleSubmit() {
setSubmitting(true);
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ text })
});
setSubmitting(false);
}
return (
<div>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={handleSubmit}
disabled={submitting}>
{submitting ? 'Saving...' : 'Submit'}
</button>
</div>
);
}
useState のセッターは、前の値を受け取って新しい値を返す関数を受け入れることができます。関数形式は、新しい状態が前の状態から計算され、バッチ化された setState 呼び出しが可能な場合に必要です — 各呼び出しは現在の前の値を受け取ります。
useEffect は、関数コンポーネントで副作用を実行するためのフックです。2つの引数を受け取ります: イフェクト関数と依存配列です。React は各レンダリング後にイフェクトを実行しますが、依存関係が指定されている場合は — そのうちのいずれかが変わったときだけ実行されます。
空の依存配列 [] は、イフェクトが最初のレンダリング後に1度だけ実行されることを意味します(componentDidMount に相当)。イフェクトから関数を返すと、それはアンマウント時(componentWillUnmount に相当)およびイフェクトの再実行前に実行されるクリーンアップです。
import React, { useState, useEffect } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const socket = new WebSocket(`wss://chat/${roomId}`);
socket.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
// クリーンアップ - ソケットを閉じる
return () => socket.close();
}, [roomId]);
return <ul>
{messages.map((msg, i) =>
<li key={i}>{msg.text}</li>
)}
</ul>;
}
useEffect の依存関係は、イフェクトと外部との唯一の接点です。イフェクトが props または状態から変数を使用する場合、その変数は依存配列に入れる必要があります。このルールを無視すると、ステールクロージャー(stale closure)によるバグの原因になります。
useContext は、コンテキスト値を読み取るためのフックです。React.createContext で作成されたコンテキストオブジェクトを受け取り、現在の値を返します。コンテキストが変わると、コンポーネントは自動的に新しい値で再レンダリングされます。
useContext 以前は、関数コンポーネントでコンテキストにアクセスするには Context.Consumer でラップする必要があり、それによってネストが生じました。useContext は Consumer を完全に削除します — 関数コンポーネントのどこででもフックを呼ぶだけです。
import React, { createContext, useContext } from 'react';
const ThemeContext = createContext('ライト');
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button
className={`btn btn-${theme}`}
>
{theme === 'dark' ? 'moon' : '☀️'}
</button>;
}
useContext は Redux やグローバル状態の代替ではありません。React はあまり変更されない値— テーマ、ロケール、認証ステータスにコンテキストを使用することを推奨しています。頻繁に更新されるデータの場合、再レンダリングごとにすべてのコンテキストコンシューマーが再計算され、パフォーマンスが低下する可能性があります。
useReducer は、複雑な更新ロジックをもつ状態に対する useState の代替です。リデューサー関数と初期状態を受け取り、現在の状態と dispatch 関数を返します。リデューサーは現在の状態とアクションを受け取り、新しい状態を返します — 副作用のない純掟関数です。
useReducer は、複数のアクション(set, reset, increment, addItem)が1つの状態を管理する場合に特に便利です。異なるセッターをもつ複数の useState の代わりに — 明確なアクションタイプをもつ1つのリデューサーです。これにより、ロジックが予測可能で、テストが簡単になります。
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'setStep':
return { ...state, step: action.step };
case 'reset':
return { count: 0, step: 1 };
default:
throw new Error('Unknown action: ' + action.type);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
step: 1
});
return (
<>
<p>Count: {state.count}</p>
<button
onClick={() => dispatch({ type: 'increment' })}>+</button>
<button
onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</>
);
}
useRef は、レンダリング間で継続する可変オブジェクトを作成します。current を変更しても再レンダリングは発生しません。主な用途は、DOM 要素へのアクセスと可変な値(タイマー、前の値、ライブラリインスタンス)の保存です。
useMemo は関数の結果をメモ化します。依存関係が変わったときだけ値を再計算します。費時のかかる計算に使用します — 大きな配列のフィルタリング、ソート、データのフォーマットなど。useCallback は、関数をメモ化するための useMemo の特別なケースです。
import React, { useRef, useMemo, useCallback } from 'react';
function SearchPage({ items, query, onItemSelect }) {
const inputRef = useRef(null);
// マウント時に自動フォーカス
useEffect(() => {
inputRef.current?.focus();
}, []);
// フィルタリング結果をメモ化
const filtered = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase()
.includes(query.toLowerCase())
);
}, [items, query]);
// コールバックをメモ化
const handleSelect = useCallback((id) => {
onItemSelect(id);
}, [onItemSelect]);
return (
<div>
<input ref={inputRef}
value={query}
onChange={e => setQuery(e.target.value)}
/>
<ul>
{filtered.map(item =>
<li key={item.id}>
<button
onClick={() => handleSelect(item.id)}>
{item.name}
</button>
</li>
)}
</ul>
</div>
);
}
重要なルール: useRef、useMemo、useCallback は最適化ツールであり、必須構造ではありません。すべてを useMemo で囲まないでください — まずパフォーマンスを測定してください。過剰な最適化はコードを複雑にします。React はコンポーネントを高速にレンダリングします; useMemo は計算が本当に費時のかかる場合(O(n²) 以上)にのみ必要です。
React はフックについて2つの厳密なルールを導入しました。ひとつ目: フックはトップレベルでのみ呼び出します — 条件分岐、ループ、またはネスト関数の内部では呼び出さないでください。ふたつ目: フックは React 関数コンポーネントまたはカスタムフックからのみ呼び出します。ESLint プラグイン eslint-plugin-react-hooks が自動的に両方のルールをチェックします。
よくある誤りはフック呼び出しの順序の違反です。React はレンダリング間のフック呼び出しの順序に依存しています。フックが条件分岐の内部にあり、あるレンダリングで実行されなかった場合、それに続くすべてのフックが屈みすれ、バグの原因となります。
| 誤り | 発生すること | 解決策 |
|---|---|---|
| 条件内のフック | フックの順序がめくれる | 条件ロジックをフック内に移動 |
| Stale closure | イフェクトが古い値を使用 | すべての依存関係を配列に追加 |
| 無限ループ | 依存関係なしの useEffect が state を変更 | 依存関係を指定または useReducer を使用 |
| レンダーでの ref 変更 | 関数ボディでの副作用 | 変更を useEffect に移動 |
2番目のよくある誤りは、useEffect でのstale closureです。イフェクトが依存関係に指定されていない変数を使用する場合、イフェクトはクロージャー作成時の値を“キャプチャ”します。解決策は、使用されているすべての変数を依存配列に必ず含めることです。依存関係が多すぎる場合は、イフェクトを複数に分割することを検討してください。
よくある質問
React は、レンダリング間のフック呼び出しの順序に依存しています。フックが条件分岐内で呼び出され、あるレンダリングで実行されなかった場合、それに続くすべてのフックが屈みすれ、状態が混乱します。ESLint ルール hooks/exhaustive-deps がこれについて警告します。
useLayoutEffect は同期で、ブラウザが変更をペイントする前に実行されます。useEffect は非同期で、ペイント後に実行されます。useLayoutEffect は、ユーザーが変更を見る前に DOM を測定または変更する必要がある場合(ポプオーバー位置、スクロール復元)にのみ使用します。
useEffect 内でAbortControllerを使用します。controller を作成し、signal を fetch に渡し、クリーンアップ関数で controller.abort() を呼びます。古いブラウザでは、cancelled フラグを使用します: return () => { cancelled = true; } setState の前にチェックを行います。
カスタムフックは、組み込みフックを使用する、use 接頭辞がついた JavaScript 関数です。同じフックを使用したロジックが2つ以上のコンポーネントで繰り返される場合に作成します。例: useDebounce, useLocalStorage, useMediaQuery。カスタムフックは HOC や render props を置き換えます。
Strict Modeでは、React が開発環境でコンポーネントを意図的に2回マウント・アンマウントし、未クリーンのイフェクトによるバグを見つけます。クリーンアップが正しく書かれていれば、2回の呼び出しは問題を引き起こしません。プロダクションモードではこれは発生しません。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。