はじめに|なぜReactカスタムフックを学ぶのか

正直に言うと、僕はこれまで React のカスタムフックを実務で使ったことはありません。
これまでは useState
と useEffect
を組み合わせて必要な処理を書き、なんとか回してきました。
ただ、しっかり調べてみると「カスタムフックに切り出すだけで、ロジックの再利用性と見通しが一気に良くなる」ことがわかりました。
フォーム入力、API 通信、イベントリスナーの登録・解除など、どのコンポーネントでも繰り返し出てくる“同じような処理”を 小さな部品(関数)として共有できるのがカスタムフックです。
この記事では、僕が調べて理解した内容をベースに、作り方と実践例を
「コード全文 → 抜粋 → ていねいな解説」
の順でまとめます。はじめての方でもスッと読めるように、言葉はできるだけやさしくしています!
Reactにおけるカスタムフックとは?
- 定義:
use
で始まる通常の関数。中でuseState
/useEffect
/useRef
など 既存のフックを組み合わせて作る。 - 役割:コンポーネントから ロジック(状態+副作用)を切り出して再利用できるようにする「箱」。
- 効果:
- 1つの責務にまとめられる(フォーム値の管理、データ取得、イベント購読など)
- UI(JSX)とロジックが分離されて コンポーネントが読みやすくなる
Reactでカスタムフックを作る基本ルール

- 関数名は
use
ではじめる(例:useCounter
,useInput
) - フックのルールを守る(トップレベルだけで呼ぶ/条件分岐やループの中で呼ばない)
- 入出力をシンプルに(返すデータや関数の形は、使う側が迷わない API に)
Reactカスタムフックの最小サンプル:useCounter
コード全文(useCounter.ts
)
import { useState, useCallback } from "react";
export function useCounter(initial = 0, step = 1) {
const [count, setCount] = useState(initial);
const increment = useCallback(() => setCount((c) => c + step), [step]);
const decrement = useCallback(() => setCount((c) => c - step), [step]);
const reset = useCallback(() => setCount(initial), [initial]);
return { count, increment, decrement, reset };
}
使い方(呼び出し側 Counter.tsx
)
import { useCounter } from "./useCounter";
export default function Counter() {
const { count, increment, decrement, reset } = useCounter(0, 1);
return (
<div>
<p>カウント: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>リセット</button>
</div>
);
}
抜粋&詳解
const [count, setCount] = useState(initial);
- フック内に状態を持つ。呼び出し側は 状態の中身だけ使える。
const increment = useCallback(() => setCount((c) => c + step), [step]);
useCallback
で 関数の参照を安定化。呼び出し側の再レンダリングを無駄に増やさない。- 依存配列に
step
を入れることで、step
が変わったときに関数を更新。
return { count, increment, decrement, reset };
- 返す API は最小限でわかりやすく。オブジェクトで返すと、使う側で名前付きで取り出せて読みやすい。
Reactでフォーム入力をまとめる:useInput
コード全文(useInput.ts
)
import { useState, useCallback } from "react";
export function useInput(initial = "") {
const [value, setValue] = useState(initial);
const onChange = useCallback(
(e) => setValue(e.target.value),
[]
);
const reset = useCallback(() => setValue(initial), [initial]);
return { value, onChange, reset, setValue };
}
使い方(ProfileForm.tsx
)
import { useInput } from "./useInput";
export default function ProfileForm() {
const name = useInput("");
const email = useInput("");
return (
<form>
<div>
<label>名前</label>
<input type="text" value={name.value} onChange={name.onChange} />
</div>
<div>
<label>メール</label>
<input type="email" value={email.value} onChange={email.onChange} />
</div>
<button type="button" onClick={() => { name.reset(); email.reset(); }}>
リセット
</button>
</form>
);
}
抜粋&詳解
const onChange = useCallback((e) => setValue(e.target.value), []);
- 入力欄の定番パターンを 1行に集約。呼び出し側は
value
とonChange
をセットで渡すだけ。
return { value, onChange, reset, setValue };
- 直接値を変えたい場面のために
setValue
も公開。使い勝手の良い API に。
Reactでウィンドウサイズを取得する:useWindowSize
コード全文(useWindowSize.ts
)
import { useState, useEffect } from "react";
export function useWindowSize() {
const isClient = typeof window !== "undefined";
const get = () => ({
width: isClient ? window.innerWidth : 0,
height: isClient ? window.innerHeight : 0,
});
const [size, setSize] = useState(get);
useEffect(() => {
if (!isClient) return;
const onResize = () => setSize(get());
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, [isClient]);
return size; // { width, height }
}
使い方(Layout.tsx
)
import { useWindowSize } from "./useWindowSize";
export default function Layout() {
const { width } = useWindowSize();
const isMobile = width < 768;
return <div>{isMobile ? "モバイル表示" : "デスクトップ表示"}</div>;
}
抜粋&詳解
const isClient = typeof window !== "undefined";
- SSR(サーバーサイドレンダリング)環境では
window
が無いので 安全に分岐。
useEffect(() => {
if (!isClient) return;
const onResize = () => setSize(get());
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, [isClient]);
- 登録したイベントはクリーンアップで必ず解除。メモリリークや重複実行を防ぐ。
Reactでカスタムフックを使うメリットと注意点
メリット
- 再利用性:複数コンポーネントで同じロジックを共有
- 見通し:JSX がスッキリし、UI とロジックが分離
- テスト容易性:ロジック単位で検証しやすい
注意点
- 過剰分割しない:そのコンポーネントでしか使わない処理まで分けると逆に読みにくい
- API を最小に:返しすぎると迷う。使う側の目線で設計
- 依存配列を正しく書く:
useEffect
/useCallback
の依存漏れはバグの温床 - 副作用の掃除を忘れない:イベントやタイマーはクリーンアップで解除
Reactカスタムフックでよくあるつまずき
- フックのルール違反
- ループや条件分岐の中で呼ばない。トップレベルだけで呼ぶ。
- 依存配列の書き忘れ/過不足
- 依存を入れ忘れると古い値(stale)を参照。入れすぎると無限ループ。
- イベント購読の解除漏れ
useEffect
のreturn
で必ず解除してリソースを解放。
- 不安定な関数参照
- 呼び出し側の再レンダリングを増やさないために
useCallback
で安定化。
- 呼び出し側の再レンダリングを増やさないために
- “なんでも屋” フック
- 1 フック 1 目的が基本。機能を詰め込みすぎない。
まとめ|Reactカスタムフックでロジックを整理しよう

- カスタムフックは 「ロジックの箱」。UI と分けて 再利用&見通しアップ。
- まずは
useCounter
やuseInput
のような 小さなフックから始め、
慣れてきたらuseWindowSize
のような 副作用系に挑戦。 - フックのルール・依存配列・クリーンアップの3点を守れば、実務でも安心して使えます。
僕自身、実装経験はこれからですが、調べて分かったのは「カスタムフックがあるだけで設計の幅が広がる」ということ。
“頭脳はフックに、見た目はコンポーネントに” を意識して、少しずつ取り入れていきます!
例えるなら「UIは見た目、カスタムフックは頭脳」。見た目と頭脳を分ければ、後から直すのもラクになります。