はじめに
Reactを学び始めて、「useStateは分かるけど、他のフックがよく分からない」と感じていませんか?
「useEffectって何のために使うの?」「useMemoとuseCallbackの違いは?」という疑問を持っている方も多いのではないでしょうか。
本記事では、Reactを始めたばかりの方を対象に、useState以外でよく使う6つのフックについて、実際のコード例を交えながら丁寧に解説します。
各フックの基本的な使い方から実践的な活用例まで、一つずつ理解していきましょう。この記事を読み終えれば、Reactのフックを自信を持って使えるようになります。
フックとは何か?基本を理解しよう
フックとは、React 16.8で導入された、関数コンポーネントで状態や副作用を扱うための仕組みです。
簡単に言えば、「関数コンポーネントに特別な機能を追加する魔法の関数」のようなものです。クラスコンポーネントを書かなくても、状態管理やライフサイクルなどの機能が使えるようになりました。
フックの基本ルール
フックを使う際は、以下の2つのルールを必ず守る必要があります。
- トップレベルでのみ呼び出す – 条件文やループの中では使えません
- Reactの関数内でのみ呼び出す – 通常のJavaScript関数では使えません
これらのルールを守ることで、Reactがフックの呼び出し順序を正しく管理できるようになります。
1. useEffect – 副作用を扱うフック
useEffectは、コンポーネントの副作用を扱うためのフックです。
なぜuseEffectが必要?
ReactでuseEffectが必要な理由は「副作用(データ取得やイベントリスナー設定など)」を安全に、かつ効率的に扱うためです。
もしuseEffectを使わなかった場合、コンポーネントがレンダリングされるたびに処理が実行されてしまいます。そうなると無限ループが発生したり、パフォーマンスが落ちたり、メモリリーク(不要なリソースが解放されない状態)につながる危険があります。
一方で、useEffectを使えば以下のメリットがあります。
- タイミングを制御できる
レンダリングの後にだけ実行する、といった制御が可能になります。 - クリーンアップ処理ができる
イベントリスナーを解除したり、タイマーを止めたりといった「後片付け」ができます。 - 依存配列で最適化できる
指定した値が変化したときだけ処理を実行できるので、無駄な処理を減らせます。 - 副作用をロジックから分離できる
UIの描画と、それ以外の「副作用」をわかりやすく分けることができます。
つまり、useEffectは「余計な処理を避けつつ、安全に副作用を管理するための仕組み」です。
Reactを使う上でほぼ必須の考え方です。
基本的な使い方
今回は無料の JSONPlaceholder APIを使用します。このAPIは開発者向けのテスト用APIで、登録不要ですぐに使えます。
import { useEffect, useState } from 'react';
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// コンポーネントがマウントされたときにユーザー情報を取得
useEffect(() => {
// JSONPlaceholder APIからユーザー情報を取得
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(error => {
console.error('エラー:', error);
setLoading(false);
});
}, []); // 空の配列は「初回のみ実行」を意味します
if (loading) return <p>読み込み中...</p>;
return (
<div>
{user ? (
<>
<p>こんにちは、{user.name}さん!</p>
<p>メール: {user.email}</p>
<p>会社: {user.company.name}</p>
</>
) : (
<p>ユーザー情報が見つかりません</p>
)}
</div>
);
}
依存配列の重要性
useEffectの第二引数(依存配列)によって、実行タイミングが変わります。
// 毎回実行される(依存配列なし)
useEffect(() => {
console.log('レンダリングのたびに実行');
});
// 初回のみ実行(空の依存配列)
useEffect(() => {
console.log('マウント時に一度だけ実行');
}, []);
// 特定の値が変わったときに実行
useEffect(() => {
console.log('countが変更されたときに実行');
}, [count]);
クリーンアップ処理
タイマーやイベントリスナーなど、解除が必要な処理には、クリーンアップ関数を返します。
import { useEffect, useState } from 'react';
function CountdownTimer() {
const [count, setCount] = useState(10);
useEffect(() => {
// カウントが0になったらタイマーを停止
if (count <= 0) return;
const timer = setInterval(() => {
setCount(prevCount => prevCount - 1);
}, 1000);
// クリーンアップ関数(コンポーネントがアンマウントされたときに実行)
return () => {
clearInterval(timer); // タイマーを解除
};
}, [count]); // countが変更されるたびに実行
return (
<div>
<h2>カウントダウン: {count}</h2>
{count === 0 && <p>タイムアップ!</p>}
<button onClick={() => setCount(10)}>リセット</button>
</div>
);
}
まとめ
useEffectは「Reactの外の世界」と繋がる窓みたいなものです。
DOM操作、API通信、タイマー、イベントリスナーなどReactのレンダリングサイクル外の処理を安全かつ効率的に実行するために必要不可欠です。
2. useContext – コンポーネント間でデータを共有するフック
useContext は、複数のコンポーネント間でデータを共有するためのフックです。
これを使うと「props のバケツリレー(親から子、さらに孫へと延々渡していくこと)」を避けられます。
なぜuseContextが必要?
React では通常、親コンポーネントから子コンポーネントへデータを渡すには props を使います。
しかしアプリが大きくなると、「グローバルに使いたいデータ(例: テーマ設定、ログイン中のユーザー情報など)」を深い階層まで渡すのが大変になります。
<App theme="dark">
<Layout theme="dark">
<Sidebar theme="dark">
<Menu theme="dark" />
</Sidebar>
</Layout>
</App>
このように全てのコンポーネントに theme を渡すのは面倒です。
ここで登場するのが useContext。
一度コンテキストを作ってしまえば、必要なコンポーネントから直接データを取り出せます。
基本的な使い方
例として「テーマ(light/dark)」を共有する場合を見てみましょう。
①まずはコンテキストを宣言
// src/context/ThemeContext.jsx
import { createContext, useContext, useState } from "react";
// 箱を作る
export const ThemeContext = createContext();
// 鍵を作る
export const useTheme = () => useContext(ThemeContext);
// 実際に複数のファイルで共有したい処理
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
※上記だと警告がでるので、実際はプロバイダーとコンテキストを分けるほうがいいです!
各機能は以下のようなイメージです!
createContext: 「箱を宣言」
Provider: 「箱に中身を入れて、アプリ全体に届ける」
useContext: 「箱のカギを使って、中身を取り出す」
②アプリ全体をAuthProvider でラップする
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.jsx";
import { ThemeProvider } from "./context/ThemeContext.jsx";
createRoot(document.getElementById("root")).render(
<StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</StrictMode>
);
③実際に使ってみる(例: ThemedButton.jsx)
// src/components/ThemedButton.jsx
import { useTheme } from "../context/ThemeContext";
export default function ThemedButton() {
const { theme, setTheme } = useTheme();
return (
<button
style={{
background: theme === "light" ? "#fff" : "#333",
color: theme === "light" ? "#000" : "#fff",
}}
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
現在のテーマ: {theme}
</button>
);
}
まとめ
useContextは 「グローバルに共有したいデータ」 に便利propsのバケツリレーを解消できる- 典型例は「テーマ設定」や「ユーザー認証」、「ダークモード切り替え」など
- Provider でラップしないと使えないので注意
3. useMemo – 計算結果をメモ化して最適化
useMemo は、重い計算処理の結果をキャッシュ(メモ化)して、パフォーマンスを最適化するためのフックです。
なぜuseMemoが必要?
コンポーネントが再レンダリングされるたびに重い計算を実行すると、アプリが遅くなります。useMemo を使うと「特定の値が変わったときだけ再計算」できるため、無駄な処理を防げます。
基本的な使い方
以下の例では、count が変わったときだけ“ゆっくり合計”を再計算します。
テキスト入力では再計算されません。
import { useMemo, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// countが変わったときだけ再計算される
const expensiveValue = useMemo(() => {
// わざと時間がかかるっぽい処理
let sum = 0;
for (let i = 0; i < count * 50_000; i++) {
sum += i;
}
return sum;
}, [count]);
return (
<div>
<h3>デモ</h3>
<p>count: {count}</p>
<p>sum: {expensiveValue}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<button onClick={() => setCount((c) => Math.max(0, c - 1))}>-1</button>
<br />
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="テキスト入力(再計算されません)"
style={{ marginTop: 8 }}
/>
</div>
);
}
実践的な使い方
「配列のフィルタ処理」をメモ化します。
検索ワードや元データが変わったときだけ再計算されます。
import { useMemo, useState } from "react";
function FruitList() {
const [fruits] = useState([
"apple",
"banana",
"grape",
"orange",
"peach",
"melon",
]);
const [searchTerm, setSearchTerm] = useState("");
const filtered = useMemo(() => {
const q = searchTerm.trim().toLowerCase();
if (!q) return fruits;
return fruits.filter((f) => f.toLowerCase().includes(q));
}, [fruits, searchTerm]);
return (
<div>
<h3>フルーツ検索(useMemoでフィルタを最適化)</h3>
<input
type="text"
placeholder="例: ap, an など"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ marginBottom: 8 }}
/>
<ul>
{filtered.map((f) => (
<li key={f}>{f}</li>
))}
</ul>
{filtered.length === 0 && <p>該当するフルーツが見つかりません</p>}
</div>
);
}
まとめ
- useMemo は計算結果をメモ化して無駄な再計算を防ぐ
- 再レンダリングが頻繁に起きるコンポーネントで効果を発揮
- 配列のフィルタリングやソート、重い計算処理などに最適
4. useCallback – 関数をメモ化して再生成を防ぐ
useCallback は、関数をメモ化して不要な再生成を防ぐためのフックです。
特に「関数を子コンポーネントに渡すとき」に使うと効果的です。
なぜuseCallbackが必要?
React では、コンポーネントが再レンダリングされるたびに関数も毎回新しく作られます。
もしその関数を子コンポーネントに渡していると、関数が新しくなったことで「子コンポーネントも再レンダリングされる」という無駄が発生します。
useCallback を使えば、依存する値が変わらない限り同じ関数を再利用できるため、不要な再レンダリングを防ぐことができます。
基本的な使い方
import { useCallback, useState, memo } from "react";
// メモ化された子コンポーネント
const ChildButton = memo(({ onClick }) => {
console.log("子コンポーネントがレンダリングされました");
return <button onClick={onClick}>クリック</button>;
});
export default function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// countが変わったときだけ再生成される
const handleClick = useCallback(() => {
console.log(`現在のカウント: ${count}`);
}, [count]);
return (
<div>
<h3>useCallbackデモ</h3>
<ChildButton onClick={handleClick} />
<button onClick={() => setCount((c) => c + 1)}>カウント増加: {count}</button>
<br />
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="テキスト入力(子は再レンダリングされません)"
style={{ marginTop: 8 }}
/>
</div>
);
}
この例では、handleClick は count が変わらない限り同じ関数として再利用されます。
そのため、text を入力しても ChildButton は再レンダリングされません。
useMemoとの違い
- useMemo → 「値」をメモ化
- useCallback → 「関数」をメモ化
実際には、以下のように useCallback は useMemo の一種と考えられます。
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
// ↑は実質的にこう書くのと同じ
const memoizedCallback = useMemo(() => () => doSomething(a, b), [a, b]);
まとめ
useCallbackは関数の再生成を防ぐためのフック- 特に子コンポーネントに関数を渡すときに有効
useMemoは値のメモ化、useCallbackは関数のメモ化- 不要な再レンダリングを減らすことでパフォーマンスを最適化できる
5. useReducer – 複雑な状態管理を整理するフック
useReducer は、複数の状態とその更新ルールを一か所にまとめて管理できるフックです。
フォーム送信やAPI通信のように「状態が複雑に絡み合う処理」でとても役立ちます。
なぜ useReducer が必要?
たとえばデータを取得するとき、こんな3つの状態を扱います。
- 読み込み中かどうか(loading)
- 取得したデータ(post)
- エラーが発生したかどうか(error)
これを useState でそれぞれ管理すると、更新のたびに複数の関数を呼ぶ必要があります。
「成功したらAとBを更新して、失敗したらCも変える」などの処理が増えると、コードが煩雑になります。
useReducer を使えば、状態の変化を**一つのルール関数(reducer)**に集約できます。
useState 版のコード例
import { useState } from "react";
export default function App() {
const [loading, setLoading] = useState(false);
const [post, setPost] = useState(null);
const [error, setError] = useState(false);
const handleClick = () => {
setLoading(true);
setError(false);
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.json())
.then((json) => {
setPost(json);
setLoading(false);
})
.catch((err) => {
console.error(err);
setError(true);
});
};
return (
<div>
{loading ? <p>読み込み中...</p> : <p>{post?.title}</p>}
<p>{error && "エラー"}</p>
<button onClick={handleClick}>取得</button>
</div>
);
}
状態が3つあるだけでも、成功・失敗・通信中のそれぞれで関数呼び出しが多くなっています。
useReducer 版の書き方
useReducer を使うと、次のように書けます。
import { useReducer } from "react";
// 初期状態
const initialState = {
loading: false,
post: null,
error: false,
};
// 状態更新のルール(reducer関数)
function reducer(state, action) {
switch (action.type) {
case "FETCH_START":
return { loading: true, post: null, error: false };
case "FETCH_SUCCESS":
return { loading: false, post: action.payload, error: false };
case "FETCH_ERROR":
return { loading: false, post: null, error: true };
default:
return state;
}
}
export default function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const handleClick = () => {
dispatch({ type: "FETCH_START" });
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((res) => res.json())
.then((json) => {
dispatch({ type: "FETCH_SUCCESS", payload: json });
})
.catch((err) => {
console.error(err);
dispatch({ type: "FETCH_ERROR" });
});
};
const { loading, post, error } = state;
return (
<div>
{loading ? <p>読み込み中...</p> : <p>{post?.title}</p>}
<p>{error && "エラー"}</p>
<button onClick={handleClick}>取得</button>
</div>
);
}
どう変わったか?
- 状態をまとめて管理
loading、post、errorの3つを1つのstateに統合。 - 状態更新のロジックを reducer に集約
成功・失敗・開始などのルールをひとまとめにできる。 - コンポーネントがシンプルになる
画面側ではdispatchを呼ぶだけで、状態更新が自動的に整理される。
仕組みの流れを理解しよう
- ボタンをクリックすると
dispatch({ type: "FETCH_START" })が呼ばれる reducerが動いてloading: trueに変わる- fetch が成功したら
dispatch({ type: "FETCH_SUCCESS", payload: json }) - 失敗したら
dispatch({ type: "FETCH_ERROR" })
これだけで、状態管理がすべて1か所にまとまります。
useStateとの使い分け
| フック名 | 特徴 | 向いているケース |
|---|---|---|
| useState | シンプルに1つの値を管理 | カウンター、トグル、フォーム1項目など |
| useReducer | 状態が複雑で関係し合う | データ取得、フォーム、カートなど |
まとめ
useState は手軽ですが、状態が3つ以上になると管理が大変になります。useReducer は「どんな操作でどう変わるか」を明確に書けるため、複雑なロジックを整理するのに最適です。
6. useRef – DOM要素への参照と値の保持
useRef は、DOM要素に直接アクセスしたり、再レンダリングを引き起こさずに値を保持するためのフックです。
なぜuseRefが必要?
React では通常、UIを「状態(state)」を通じて制御しますが、
次のような場合には、DOM要素そのものに直接アクセスしたいことがあります。
- 入力欄にフォーカスを当てたい
- スクロール位置を取得したい
- タイマーIDなど、UIに関係ない値を保持したい
useState を使うと値が変わるたびに再レンダリングされますが、useRef は値が変わっても再レンダリングされないため、パフォーマンスに優れた一時保存に適しています。
基本的な使い方① DOM要素へのアクセス
入力欄に自動的にフォーカスを当てるシンプルな例です。
import { useEffect, useRef } from "react";
export default function FocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// コンポーネントが表示された直後にフォーカスを当てる
inputRef.current.focus();
}, []);
return <input ref={inputRef} placeholder="自動でフォーカスされます" />;
}
inputRef.current に、実際のDOM要素(<input>)が格納されます。
この current プロパティを通じて、直接DOM操作が可能になります。
基本的な使い方② 再レンダリングしない値の保持
useRef で保存した値は変更しても再レンダリングが起こりません。
例えばタイマーIDのような一時的な値を保持する場合に便利です。
import { useRef, useState } from "react";
export default function Timer() {
const [count, setCount] = useState(0);
const timerRef = useRef(null);
const start = () => {
if (timerRef.current) return; // すでに実行中なら何もしない
timerRef.current = setInterval(() => setCount((c) => c + 1), 1000);
};
const stop = () => {
clearInterval(timerRef.current);
timerRef.current = null;
};
const reset = () => {
stop();
setCount(0);
};
return (
<div>
<p>経過時間: {count}秒</p>
<button onClick={start}>開始</button>
<button onClick={stop}>停止</button>
<button onClick={reset}>リセット</button>
</div>
);
}
useRef にタイマーIDを保存しておくことで、setInterval を複数回呼ばないよう制御できます。
この処理を useState で書くと、タイマーを止めるたびに不要な再レンダリングが起きてしまいます。
useStateとの違い
| 比較項目 | useState | useRef |
|---|---|---|
| 値の変更時に再レンダリング | される | されない |
| 主な用途 | UIの更新に使う | DOM操作・一時的な値保持 |
| 代表的な利用例 | カウンター、フォームの値 | タイマーID、フォーカス制御、前回の値の保持 |
まとめ
useRefは「DOM操作」と「再レンダリングしない値の保持」に使う.currentにアクセスして要素や値を操作できる- UIに関係しない一時的な値を保持したいときに最適
さいごに
ここまで、Reactでよく使われる6つのフックについて解説しました。
- useEffect:副作用(API通信・イベント登録など)を安全に扱う
- useContext:コンポーネント間でデータを共有する
- useMemo:重い計算結果をメモ化して最適化
- useCallback:関数の再生成を防いでパフォーマンスを向上
- useReducer:複雑な状態管理を整理して保守性を高める
- useRef:DOM操作や再レンダリングしない値の保持に利用
最初は「どのフックをいつ使うのか」が分かりづらいかもしれません。
しかしそれぞれのフックには「何を解決するためのものか」という明確な目的があります。
このように「目的から選ぶ」ように意識すると、自然とフックが使いこなせるようになります。
Reactのフックは、一見難しく感じても、触って慣れることで必ず理解が深まります。
ぜひこの記事を参考に、実際にコードを書きながら、自分の中に落とし込んでいきましょう。