React Hooks は React 16.8 で導入された機能で、関数コンポーネント内で状態管理や副作用を扱うための仕組みです。
Hooks の登場によって、以前はクラスコンポーネントでしか扱いづらかった「状態」「ライフサイクル」「ロジックの再利用」を、関数ベースで自然に書けるようになりました。
この記事では、代表的な Hooks の役割を整理しつつ、「なぜその Hook が必要なのか」までソフトウェア開発者向けに分かりやすく説明します。
Hooks登場以前の課題
Hooks がない時代の React では、状態や副作用を扱うために主にクラスコンポーネントを使っていました。しかし、実務では次のような課題がありました。
ロジックの再利用性の低下
例えば「画面サイズを監視する」「API を呼び出して loading / error / data を管理する」といったロジックを複数の画面で使いたくても、クラスコンポーネントではきれいに共通化しにくい問題がありました。
主な再利用手段としては HOC(Higher-Order Component)や Render Props がありましたが、次のようなデメリットがありました。
- コンポーネントのネストが深くなる
- props の流れが追いづらくなる
- UI とロジックの責務が分かりにくくなる
巨大なクラスコンポーネント
クラスコンポーネントでは、状態は state、副作用は componentDidMount や componentDidUpdate、後始末は componentWillUnmount というように、関心ごとごとではなくライフサイクルごとにコードが分散しがちでした。
その結果、1つの機能に関する処理が複数メソッドに分かれ、次のような状態に陥りやすくなります。
- 同じ目的の処理が複数箇所に散らばる
- 関連するコードをまとめて読めない
- 保守時に変更漏れが起きやすい
Hooksの解決アプローチ
Hooks は、こうした課題を「関数コンポーネントのまま状態と副作用を扱えるようにする」ことで解決しました。
ポイントは以下の通りです。
- 状態管理を
useStateで書ける - 副作用を
useEffectで書ける - DOM 参照やミュータブルな値の保持を
useRefで書ける - コンポーネント間で値を共有する仕組みを
useContextで書ける - 複雑な状態遷移を
useReducerで整理できる - ロジックをカスタム Hook として再利用できる
特に重要なのは、UI ではなくロジック単位で再利用しやすくなったことです。
例えば「フォーム入力の管理」「ユーザー情報の取得」「スクロール位置の監視」といった処理を useXxx という形の関数にまとめることで、複数コンポーネント間で自然に共有できます。
なぜトップレベルでのみHooksを呼び出せるのか?
React では、Hooks を必ずコンポーネントのトップレベルで呼び出す必要があります。if 文や for 文、ネストした関数の中では呼び出してはいけません。
これは、React が Hook の呼び出し順序 を使って状態を管理しているためです。
例えば以下のようなコードがあったとします。
function Example({ visible }: { visible: boolean }) {
const [count, setCount] = useState(0);
if (visible) {
useEffect(() => {
console.log("visible");
}, []);
}
const [name, setName] = useState("");
}
この場合、visible が true のときと false のときで Hook の呼び出し回数が変わってしまいます。すると React は「2番目の Hook が何だったか」を正しく対応づけられなくなります。
そのため Hooks には次のルールがあります。
- Hooks は関数コンポーネントのトップレベルで呼ぶ
- Hooks は React の関数コンポーネントかカスタム Hook の中だけで呼ぶ
このルールは単なる書き方の制約ではなく、React の内部実装に基づく重要な前提です。
1. useState
useState は、関数コンポーネントに状態を持たせるための Hook です。
最も基本的な Hook であり、「ユーザー入力」「開閉状態」「選択中の値」など、画面の変化に応じて更新される値を扱うときに使います。
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
useState(0) は、初期値が 0 の状態を作ります。戻り値は次の2つです。
- 現在の状態
count - 状態を更新する関数
setCount
useState のポイント
- 状態が変わると再レンダリングされる
- 更新関数を使って状態を変更する
- 更新は即時反映されるとは限らず、React がまとめて処理することがある
前の状態をもとに更新する場合は、関数形式で書くのが安全です。
setCount((prev) => prev + 1);
この書き方を使うと、複数回の更新がまとめられる場面でも意図通りに動作しやすくなります。
2. useEffect
useEffect は、レンダリングとは別に実行したい処理、つまり副作用を扱うための Hook です。
API 通信や DOM 操作、イベント購読、タイマー設定などの副作用を、レンダリングとは別のタイミングで実行するために使います。
import { useEffect, useState } from "react";
export function UserList() {
const [users, setUsers] = useState<string[]>([]);
useEffect(() => {
async function fetchUsers() {
const res = await fetch("/api/users");
const data = await res.json();
setUsers(data);
}
fetchUsers();
}, []);
return (
<ul>
{users.map((user) => (
<li key={user}>{user}</li>
))}
</ul>
);
}
第2引数の依存配列によって、Effect の再実行条件を制御できます。
-
[]: 初回マウント時に1回だけ実行 -
[value]:valueが変わったときに実行 - 省略: 毎回のレンダリング後に実行
なぜ副作用は useEffect 内で実行しないといけないのか?
React のレンダリング処理は、同じ入力に対して同じ UI を返す純粋な計算であることが期待されています。
もしレンダリング中に次のような副作用を直接実行すると、描画のたびに予期しない動作が起こります。
- API を何度も呼んでしまう
- DOM を不正なタイミングで操作してしまう
- イベントリスナーを重複登録してしまう
つまり、レンダリングは「何を表示するか」を決める処理であり、「外部世界に影響を与える処理」は分離しなければなりません。そのための場所が useEffect です。
クリーンアップも重要
イベントリスナーやタイマーなどは、不要になったら解除する必要があります。その場合は、Effect からクリーンアップ関数を返します。
useEffect(() => {
const id = setInterval(() => {
console.log("tick");
}, 1000);
return () => {
clearInterval(id);
};
}, []);
「登録」と「解除」を同じ場所に書けるのは、useEffect の大きな利点です。
3. useRef
useRef は、レンダリングをまたいで値や DOM を保持することができる Hook です。
大きく2つの用途があります。
- DOM 要素を参照する
- 再レンダリングを発生させずに値を保持する
DOM を参照する例
import { useEffect, useRef } from "react";
export function SearchInput() {
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}
値を保持する例
import { useEffect, useRef } from "react";
export function Timer() {
const renderCountRef = useRef(0);
useEffect(() => {
renderCountRef.current += 1;
console.log(renderCountRef.current);
});
return <div>render count is shown in console</div>;
}
useRef の .current を更新しても再レンダリングは発生しません。
そのため、画面に表示したい値は useState、表示には使わないが保持したい値は useRef、と使い分けるのが基本です。
4. useContext
useContext は、Context に入れた値を子孫コンポーネントから受け取るための Hook です。
props を何段階も渡していく props drilling を避けたいときに使います。テーマ、ログインユーザー情報、表示言語のように、複数コンポーネントで共有したい値と相性がよいです。
import { createContext, useContext } from "react";
const ThemeContext = createContext<"light" | "dark">("light");
function ThemeLabel() {
const theme = useContext(ThemeContext);
return <p>current theme: {theme}</p>;
}
export function App() {
return (
<ThemeContext.Provider value="dark">
<ThemeLabel />
</ThemeContext.Provider>
);
}
useContext 自体は状態管理をする Hook ではなく、Context に入っている値を読むための Hook です。
そのため、値を更新したい場合は useState や useReducer と組み合わせて使うことが多くなります。
5. useReducer
useReducer は、状態の更新ルールを reducer 関数として外に出し、状態遷移を整理しながら管理するための Hook です。
更新パターンが増えて useState だけでは見通しが悪くなる場面で役立ちます。特に、フォーム入力、非同期処理の状態、複数フィールドを持つ UI などで有効です。
import { useReducer } from "react";
type State = {
count: number;
};
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
default:
return state;
}
}
export function CounterWithReducer() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+1</button>
<button onClick={() => dispatch({ type: "decrement" })}>-1</button>
<button onClick={() => dispatch({ type: "reset" })}>reset</button>
</div>
);
}
useReducer を使うと、「どのイベントで、どう状態が変わるのか」を reducer に集約できます。
状態更新の分岐が多いときほど、setState をあちこちに書くより読みやすくなります。
6. useMemo
useMemo は、計算結果をメモ化するための Hook です。
レンダリングのたびに毎回同じ重い計算を実行したくないときや、オブジェクトや配列の参照を安定させたいときに使います。
import { useMemo } from "react";
export function PriceSummary({ prices }: { prices: number[] }) {
const total = useMemo(() => {
return prices.reduce((sum, price) => sum + price, 0);
}, [prices]);
return <p>Total: {total}</p>;
}
React.memo で最適化された子コンポーネントに props を渡す場合、毎回新しい配列やオブジェクトを作ると、見た目の値が同じでも「別の参照」として扱われます。
その結果、本来はスキップできたはずの再レンダリングが起こることがあります。
const filters = useMemo(() => ({ published: true }), []);
このように useMemo を使うことで、不要な再計算や、React.memo と組み合わせたときの不要な再レンダリングを減らせる場合があります。
ただし、useMemo は万能ではありません。軽い計算に対して多用すると、かえって可読性を下げることがあります。
7. useCallback
useCallback は、関数そのものをメモ化するための Hook です。
React はレンダリングの度に関数を再作成するため、不要な関数の再生成を避けたい場合に useCallback を使います。
import React, { useCallback, useState } from "react";
export function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
return (
<div>
<button onClick={() => setCount((prev) => prev + 1)}>count up</button>
<Child onClick={handleClick} />
</div>
);
}
const Child = React.memo(function Child({
onClick,
}: {
onClick: () => void;
}) {
return <button onClick={onClick}>child button</button>;
});
特に次のような場面で意味があります。
- 子コンポーネントが
React.memoで最適化されている - 依存配列に渡す関数の参照を安定させたい
- 毎回同じ関数を作り直したくない明確な理由がある
なお、useCallback もパフォーマンス最適化が目的であり、必要以上に使うことは避けるべきです。
まずは素直に書き、再レンダリングや参照の変化が問題になった箇所だけに適用するのが基本です。
番外編. カスタムHooksの作成と利用
カスタム Hook は、複数の Hook を組み合わせた再利用可能な関数です。名前は慣例的に use で始めます。
カスタムHooksが欲しくなる場面
次のようなロジックを複数コンポーネントで繰り返し書いているなら、カスタム Hook を検討する価値があります。
- API 通信と loading / error 管理
- フォーム入力の状態管理
- イベントリスナーの登録と解除
- ブラウザ状態の監視(画面幅、オンライン状態、スクロール位置など)
カスタムHooksの例
以下は、オンライン状態を監視するカスタム Hook です。
import { useEffect, useState } from "react";
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
return isOnline;
}
利用側はとてもシンプルになります。
function StatusBadge() {
const isOnline = useOnlineStatus();
return <p>{isOnline ? "Online" : "Offline"}</p>;
}
カスタムHooksの良さ
- UI とロジックを分離できる
- 再利用しやすい
- テストや保守の単位を整理しやすい
特に実務では、「見た目は違うがロジックは同じ」ケースが多くあります。そのときカスタム Hook に切り出すと、コンポーネントを薄く保ちながら共通化できます。
まとめ
React Hooks は、単に関数コンポーネントで状態を持てるようにした仕組みではありません。
状態・副作用・参照・共有状態・メモ化・ロジック再利用を、関心ごとごとに整理して書けるようにした設計だと捉えると理解しやすくなります。
まずは以下の順番で理解すると実務でも使いやすいです。
-
useStateで状態を持つ -
useEffectで副作用を分離する -
useRefで DOM やミュータブルな値を保持する -
useContextで複数コンポーネント間の値共有を理解する -
useReducerで複雑な状態遷移を整理する - 必要なときだけ
useMemo/useCallbackで最適化する - 繰り返し登場するロジックをカスタム Hook に切り出す
Hooks を使いこなせるようになると、React のコードは「UI を返す関数」と「再利用可能なロジック」の組み合わせとして整理しやすくなります。React を学ぶ上で避けて通れない重要な概念なので、まずは小さなコンポーネントから実際に触ってみるのがおすすめです。