React Hooks 完全ガイド:いつ使うか・使わないと何が起きるか
2026-07-03
#React#フロントエンド#JavaScript#パフォーマンス
「useMemo って全部につければいいんですよね?」「useCallback 入れたのに子が再レンダーされる……」——React を書いていると一度は当たる疑問と詰まりポイントを、バグ例と修正コードを並べながら整理する。
Hook の使い所チートシート
| Hook | 何をするか | 使わないと |
|---|---|---|
useState |
レンダーをトリガーする可変値 | 値が変わっても画面が更新されない |
useEffect |
副作用の実行とクリーンアップ | リーク・無限ループ・古い値の参照 |
useMemo |
重い計算結果・値参照のキャッシュ | 毎レンダーで再計算される |
useCallback |
関数参照の安定化 | React.memo の子が毎回再レンダー |
useRef |
レンダーをまたぐ可変値・DOM参照 | 再レンダーで値が消える、または不要な再レンダーが起きる |
useContext |
コンポーネントツリー越しの値共有 | props リレーが深くなる |
useState
基本
useState は「変更したら再レンダーをトリガーする値」を持つためのフック。setter を呼ばないと React は変化に気づかない。
const [count, setCount] = useState(0);
バグ①:配列・オブジェクトを直接変更する
// BAD: 同じ参照を渡しても React は変化を検知しない
const [items, setItems] = useState([1, 2, 3]);
function addItem() {
items.push(4); // 配列を直接変更
setItems(items); // 同じ参照を渡している → 再レンダーしない
}
// GOOD: 新しい配列・オブジェクトを返す
setItems([...items, 4]);
setItems(prev => [...prev, 4]); // 前の値に依存するときはこちら
バグ②:前の state に依存した更新
// BAD: 連続で呼ぶと意図した値にならないことがある
setCount(count + 1);
setCount(count + 1); // count はどちらも古い値を読んでいる
// GOOD: 前の値を受け取る関数形式
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 確実に +2 になる
useEffect
useEffect は「render の外側で起きること」を扱う。fetch、イベント登録、タイマー、外部ライブラリとの連携など。
基本構造
useEffect(() => {
// Effect 本体:DOM 更新後に実行
return () => {
// クリーンアップ:次の Effect 実行前・アンマウント時に実行
};
}, [/* 依存配列 */]);
バグ①:依存配列にオブジェクトを入れる(無限ループ)
// BAD: options は毎レンダーで新しいオブジェクト → Effect が毎回動く
function UserList({ page }) {
const options = { page, limit: 20 }; // 毎レンダーで新参照
useEffect(() => {
fetch('/api/users', { body: JSON.stringify(options) })
.then(r => r.json())
.then(setUsers);
}, [options]); // options が変わり続ける → 無限ループ
}
// GOOD: プリミティブを依存配列に入れる
useEffect(() => {
fetch(`/api/users?page=${page}&limit=20`)
.then(r => r.json())
.then(setUsers);
}, [page]); // page という数値だけ見る
バグ②:クリーンアップなしのリーク
コンポーネントがアンマウントされても fetch は走り続ける。アンマウント後に setState が呼ばれると React 警告(またはバグ)になる。
// BAD: アンマウントされても fetch が続く
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData);
}, []);
// GOOD: AbortController でキャンセル
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(e => {
if (e.name !== 'AbortError') throw e;
});
return () => controller.abort();
}, []);
バグ③:依存配列の省略
// BAD: count を読んでいるのに依存配列が空 → 古い count を参照し続ける
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 常に初期値(0)が出る
}, 1000);
return () => clearInterval(id);
}, []); // count が変わっても Effect は再実行されない
// GOOD: count を依存配列に入れる
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(id);
}, [count]);
// または useRef で最新値を参照する(Effect の再実行を避けたい場合)
const countRef = useRef(count);
useEffect(() => { countRef.current = count; }, [count]);
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current); // 常に最新値
}, 1000);
return () => clearInterval(id);
}, []); // インターバルは一度だけ作る
useMemo
「再レンダーのたびに重い計算を走らせたくない」または「値の参照を安定させたい」ときに使う。
ケース①:重い計算のキャッシュ
// BAD: 10万件のフィルタが毎レンダーで実行される
function ProductList({ products, search }) {
const filtered = products.filter(p =>
p.name.toLowerCase().includes(search.toLowerCase())
);
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
// GOOD: products か search が変わったときだけ再計算
function ProductList({ products, search }) {
const filtered = useMemo(
() => products.filter(p =>
p.name.toLowerCase().includes(search.toLowerCase())
),
[products, search]
);
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
ケース②:参照を安定させて useEffect の暴走を防ぐ
// BAD: config は毎レンダーで新しいオブジェクト → useEffect が毎回動く
function Chart({ data }) {
const config = { color: 'steelblue', width: 600 }; // 毎レンダーで新参照
useEffect(() => {
renderChart(data, config);
}, [data, config]); // config が変わり続ける → 毎回実行
}
// GOOD: 依存が変わらない限り同じ参照
function Chart({ data }) {
const config = useMemo(
() => ({ color: 'steelblue', width: 600 }),
[] // 変わらないので空
);
useEffect(() => {
renderChart(data, config);
}, [data, config]); // data が変わったときだけ実行
}
いつ useMemo は不要か
- 単純な計算(文字列結合、short array map など)
- レンダー頻度が低いコンポーネント
- メモ化のコスト(比較処理)が計算コストより高いケース
「とりあえず全部 useMemo」はオーバーエンジニアリング。重い計算や参照が問題になってから入れる。
useCallback
関数参照を安定させる。useMemo(() => fn, deps) の関数版。React.memo と組み合わせて初めて効果がある。
なぜ useCallback が必要か
// BAD: handleClick は毎レンダーで新しい関数参照になる
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('clicked');
};
return (
<>
<button onClick={() => setCount(c => c + 1)}>カウント: {count}</button>
<Child onClick={handleClick} />
</>
);
}
// React.memo で囲んでいても、毎回 handleClick が新参照なので再レンダーされる
const Child = React.memo(function Child({ onClick }) {
console.log('Child レンダー');
return <button onClick={onClick}>子ボタン</button>;
});
// GOOD: useCallback で参照を安定させる
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // 依存なし = 同じ関数を使い続ける
これで count が変わっても handleClick の参照は変わらず、Child はスキップされる。
セットで必要なもの
| 組み合わせ | 結果 |
|---|---|
useCallback だけ |
参照は安定するが子は React.memo なしなので毎回レンダー |
React.memo だけ |
毎レンダーで新しい関数参照が渡る → props 変化と判断 → 再レンダー |
| 両方 | 参照が安定 → props 変化なし → 子をスキップ |
state を参照する場合
// BAD: count を読んでいるのに依存配列が空 → 古い count を参照し続ける
const handleSave = useCallback(() => {
saveToServer(count); // 常に初期値
}, []);
// GOOD: 依存配列に count を入れる(count が変わると関数が作り直される)
const handleSave = useCallback(() => {
saveToServer(count);
}, [count]);
useRef
再レンダーをトリガーせずに値を保持する。DOM 操作にも使う。
ケース①:DOM 参照
function SearchInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus(); // マウント後にフォーカス
}, []);
return <input ref={inputRef} />;
}
ケース②:インターバル ID などを保持する
// BAD: useState で interval ID を持つと setInterval のたびに再レンダーが起きる
const [intervalId, setIntervalId] = useState<number | null>(null);
// GOOD: useRef は値が変わっても再レンダーしない
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef<number | null>(null);
const start = () => {
intervalRef.current = window.setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
}
};
return (
<>
<p>{count}</p>
<button onClick={start}>開始</button>
<button onClick={stop}>停止</button>
</>
);
}
ケース③:前の値を保持するカスタムフック
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value; // render 後に更新
});
return ref.current; // render 時点では前の値
}
function Counter() {
const [count, setCount] = useState(0);
const prev = usePrevious(count);
return (
<p>
{prev ?? '-'} → {count}
</p>
);
}
useContext
コンポーネントツリーを跨いで値を共有する。深い props リレーを避けるために使う。
const ThemeContext = createContext<'light' | 'dark'>('light');
function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
<ThemeContext.Provider value={theme}>
<Layout />
</ThemeContext.Provider>
);
}
function Button() {
const theme = useContext(ThemeContext); // どの階層からでも読める
return <button className={theme}>クリック</button>;
}
注意点:context が変わると全 consumer が再レンダーされる
値が頻繁に変わるものと、そうでないものを同じ context に混ぜると余計な再レンダーが増える。頻度ごとに context を分割する。
// 分けるべき例
const UserContext = createContext<User | null>(null); // 頻繁には変わらない
const NotificationContext = createContext<number>(0); // 頻繁に変わる
// 通知数が変わっても User を読んでいるコンポーネントは再レンダーされない
カスタム Hook で再利用する
複数コンポーネントで同じロジックが必要なら、カスタム Hook に切り出す。use で始まる関数の中では他の Hook を呼べる。
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then(r => {
if (!r.ok) throw new Error(`${r.status}`);
return r.json();
})
.then((d: T) => {
setData(d);
setLoading(false);
})
.catch(e => {
if (e.name !== 'AbortError') {
setError(e);
setLoading(false);
}
});
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// 使う側
function UserProfile({ id }: { id: string }) {
const { data, loading, error } = useFetch<User>(`/api/users/${id}`);
if (loading) return <p>読み込み中…</p>;
if (error) return <p>エラー: {error.message}</p>;
return <p>{data?.name}</p>;
}
まとめ:判断フロー
値を持ちたい
├─ 変わったら再レンダーが必要 → useState
└─ 再レンダーは不要(ID・ref・前の値) → useRef
fetch・イベント登録・タイマーを動かしたい → useEffect
└─ 立ち上げたリソースは return で片付ける(fetch abort・listener 解除・clearInterval)
計算コストが高い or オブジェクト参照を安定させたい → useMemo
子コンポーネントに渡す関数の参照を安定させたい → useCallback + React.memo
深い props リレーを避けたい → useContext
└─ 変化頻度が違う値は別 context に分ける
useMemo と useCallback は「まず動くものを作って、プロファイラで重いと確認してから入れる」が現実的な順序。最初から全部入れると、依存配列の管理コストが増えてバグの温床になる。