Reactフックのチートシート:すべてのフックをライブデモ付きで

Reactのフックとは、useState、useEffect、useRefのように、関数コンポーネントに状態、副作用、refを与える関数です。このチートシートでは、React 19.3のすべての組み込みフックについて、1行の用途、ライブデモ、よくある間違いを紹介し、コピーして使える12のカスタムフックも載せています。

すべての組み込みフックを一覧で

執筆時点(2026年10月)で、Reactの最新リリースは19.3です。表には、Reactの公式フックリファレンスに載っているすべてのフックに加え、個別のページで解説されている3つのReact 19のフック(use、useOptimistic、React DOMのフックuseFormStatus)を載せています。React 18と記したフックはReact 18と19で動作します。React 19と記したフックはReact 18には存在しません。下のReact 18のフックにはすべて、クリックして試せるライブデモがあります。このページはReact 18で動いているため、React 19のフックはコードだけを示しています。

全20件を表示中
フックグループ用途対応バージョン
useState状態変わると再レンダリングされる値を保持するReact 18
useReducer状態関連する多くの更新を、1つのreducer関数で管理するReact 18
useContextコンテキスト上にある最も近いプロバイダーから値を読むReact 18
useRefRefレンダリングを引き起こさない可変の値やDOMノードを保持するReact 18
useImperativeHandleRef親がrefを通じて呼べるメソッドを選ぶReact 18
useEffectエフェクト描画後に、React外部のものと同期するReact 18
useLayoutEffectエフェクトブラウザーが描画する前にレイアウトを測るReact 18
useInsertionEffectエフェクトレイアウトエフェクトの実行前にスタイルのルールを挿入する(CSS-in-JSライブラリ向け)React 18
useEffectEventエフェクトエフェクトを再実行せずに、その中で最新のpropsを読むReact 19.2
useMemoパフォーマンス重い計算の結果をレンダリング間でキャッシュするReact 18
useCallbackパフォーマンス関数をキャッシュして、メモ化された子が再レンダリングされないようにするReact 18
useTransitionパフォーマンス状態の更新を緊急でないものとして扱い、保留中のフラグを得るReact 18
useDeferredValueパフォーマンスUIの遅い部分を、速い入力より遅れて追従させるReact 18
useIdその他ラベルやARIA属性のための、安定した一意のIDを生成するReact 18
useSyncExternalStoreその他React外部にあるストアを購読するReact 18
useDebugValueその他React DevToolsでカスタムフックにラベルを付けるReact 18
useActionStateその他フォームアクションの結果と保留中の状態を追跡するReact 19
useOptimisticその他アクションの完了前に、予想される結果を表示するReact 19
useAPI条件分岐の中でも、Promiseやコンテキストを読むReact 19
useFormStatusReact DOM親フォームの保留中の状態を読むReact 19

フックには2つのルールがあります。コンポーネントかカスタムフックのトップレベルでだけ呼ぶこと(ループ、条件分岐、入れ子の関数の中では呼ばない)、そしてReactの関数コンポーネントかほかのフックからだけ呼ぶことです。useは1つ目のルールの唯一の例外で、ifブロックやループの中でも呼べます。eslint-plugin-react-hooksパッケージが両方のルールをチェックしてくれます。

状態のフック

useState

役割:このコンポーネントのための値を保存します。セッターを呼ぶと、新しい値で再レンダリングが予約されます。

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount((c) => c - 1)}>-1</button>
      <span>{count}</span>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
    </>
  );
}

よくある間違い:1回のクリックでsetCount(count + 1)を3回書き、+3になると思い込むこと。3つとも、このレンダリング時点の同じcountを読むので、結果は+1です。次の値が前の値に依存する場合は、setCount(c => c + 1)のように更新関数を渡しましょう。上の「+3(更新関数)」ボタンがまさにそれを行っています。もう1つの定番は、オブジェクトや配列をその場で書き換えることです(user.name = "x"; setUser(user))。Reactは参照で比較するので、同じオブジェクトだと判断してレンダリングを省きます。代わりに新しいオブジェクトを作りましょう:setUser({ ...user, name: "x" })。

useReducer

役割:更新のロジックを1つの純粋関数に移します。この関数は現在の状態とアクションを受け取り、次の状態を返します。複数の値が一緒に変わるときに便利です。

type Action = { type: "add"; text: string } | { type: "toggle"; id: number };

function reducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case "add":
      return [...state, { id: Date.now(), text: action.text, done: false }];
    case "toggle":
      return state.map((t) => (t.id === action.id ? { ...t, done: !t.done } : t));
  }
}

const [todos, dispatch] = useReducer(reducer, []);
dispatch({ type: "add", text: "Ship it" });

よくある間違い:reducerの中で副作用を行うこと(データの取得、localStorageへの書き込み、後で頼りにするタイマーからのランダムなIDの生成など)。開発時のStrict Modeでは、純粋でないコードをあぶり出すために、Reactがreducerを2回呼びます。reducerは純粋に保ち、副作用はイベントハンドラーかエフェクトに置きましょう。

コンテキストのフック

useContext

役割:このコンポーネントの上にある、最も近い対応するプロバイダーの値を読み、その値が変わると再レンダリングします。

const ThemeContext = createContext<"light" | "dark">("light");

function App() {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  return (
    <ThemeContext.Provider value={theme}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Badge() {
  const theme = useContext(ThemeContext);
  return <span className={theme}>Current theme: {theme}</span>;
}

よくある間違い:value={{ user, setUser }}のように、レンダリングのたびに新しいオブジェクトリテラルをプロバイダーの値として渡すこと。レンダリングごとに新しいオブジェクトが作られるので、何も変わっていなくても、すべての利用側が再レンダリングされます。値をuseMemoで包むか、めったに変わらない値と頻繁に変わる値を2つのコンテキストに分けましょう。また、プロバイダーがコンポーネントの上にあることも確認してください。プロバイダーを描画しているのと同じコンポーネントでuseContextを呼ぶと、既定値が返ります。

Refのフック

useRef

役割:再レンダリングをまたいで残る箱(ref.current)を提供します。中身を変えてもレンダリングは起きません。DOMノード、タイマーのID、そのほか画面に表示しない値に使います。

const inputRef = useRef<HTMLInputElement>(null);
const clicks = useRef(0);

<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
<button onClick={() => { clicks.current += 1; }}>Count silently</button>

よくある間違い:JSXの中でref.currentを表示し、それが更新されると期待すること。refはレンダリングを引き起こさないので、画面が追いつくのはほかの何かが再レンダリングしたときだけです(上の「refに加算」ボタンを押してから「再レンダリング」を押してみてください)。画面に表示すべき値なら、状態に置くべきです。また、遅延して1回だけ行う初期化を除き、レンダリング中にrefを読み書きするのは避けましょう。

useImperativeHandle

役割:親がこのコンポーネントにrefを付けたときに受け取るものをカスタマイズし、DOMノード全体ではなく、名前の付いたいくつかのメソッドだけを公開します。

type Handle = { shake: () => void; clear: () => void };

// React 18: wrap in forwardRef. In React 19 you can accept ref as a normal prop.
const FancyInput = forwardRef<Handle, Props>(function FancyInput(props, ref) {
  const el = useRef<HTMLInputElement>(null);
  useImperativeHandle(ref, () => ({
    shake() { /* animate */ el.current?.focus(); },
    clear() { if (el.current) el.current.value = ""; },
  }), []);
  return <input ref={el} />;
});

// parent
const ref = useRef<Handle>(null);
<FancyInput ref={ref} />
<button onClick={() => ref.current?.shake()}>Shake</button>

よくある間違い:propsで表現できることを、これで操作しようとすること。親がモーダルを表示するためにref.current.open()を呼んでいるなら、たいていはisOpenプロパティのほうがシンプルで追いやすくなります。命令的なハンドルは、フォーカス、スクロール、再生・一時停止、アニメーションのために取っておきましょう。依存配列(3つ目の引数)を忘れると、レンダリングのたびにハンドルが作り直されます。

エフェクトのフック

useEffect

役割:Reactが画面を更新した後にコードを実行し、コンポーネントをReact外部のもの(タイマー、購読、ネットワークリクエスト、React以外のウィジェット)と同期させます。元に戻すためのクリーンアップ関数を返します。

useEffect(() => {
  if (!running) return;
  const id = setInterval(() => setSeconds((s) => s + 1), 1000);
  return () => clearInterval(id); // runs before the next effect and on unmount
}, [running]);

よくある間違い:useEffect(() => setFullName(first + " " + last), [first, last])のように、レンダリング中に計算できるものをエフェクトで求めること。余分なレンダリングが1回増え、1フレームの間古い値が表示されることもあります。代わりにconst fullName = first + " " + lastと書きましょう。2番目に多いバグはクリーンアップの書き忘れです。開発時のStrict Modeでは、Reactが各コンポーネントをマウント、アンマウント、再マウントを1回ずつ行うので、クリーンアップのないエフェクトは購読やタイマーを2回開始してしまいます。この二重実行はヒントであって、Reactのバグではありません。

useLayoutEffect

役割:useEffectと同じ形ですが、ReactがDOMを変更した後、ブラウザーが描画する前に実行されます。要素を測ってレイアウトを調整し、利用者が誤った位置を一切目にしないようにするために使います。

const boxRef = useRef<HTMLSpanElement>(null);
const [width, setWidth] = useState(0);

useLayoutEffect(() => {
  setWidth(boxRef.current!.getBoundingClientRect().width);
}, [text]);

よくある間違い:最初からこれを使ってしまうこと。描画を止めるので、中で重い処理をするとページが固まったように感じられます。描画前の測定でしか直せないちらつき(ツールチップ、ポップオーバーの位置決め、自動サイズ調整)が見えるのでない限り、useEffectを使いましょう。また、サーバーレンダリング中は何もしないため、環境によっては警告が出ます。

useInsertionEffect

役割:あらゆるレイアウトエフェクトより前に実行されるので、ライブラリは何かがレイアウトを読む前に<style>のルールを挿入できます。CSS-in-JSライブラリの作者のためのフックです。

function useScopedRule(selector: string, rule: string) {
  useInsertionEffect(() => {
    const style = document.createElement("style");
    style.textContent = `${selector} { ${rule} }`;
    document.head.appendChild(style);
    return () => style.remove();
  }, [selector, rule]);
}

よくある間違い:アプリケーションのコードで使ったり、ここから状態を更新しようとしたりすること。この中では更新を予約できず、refもまだ紐付いていません。スタイリング用のライブラリを書いているのでなければ、ほぼ確実にuseEffectかuseLayoutEffectが適しています。

useEffectEvent

役割:React 19.2以降。関数を包み、エフェクトから呼んだときに、それらの値を依存として列挙しなくても常に最新のpropsと状態を参照できるようにします。現在のテーマでのログ記録や通知の表示など、エフェクトの「イベント」にあたる部分に使います。

// React 19.2+ only. Not run on this page.
import { useEffect, useEffectEvent } from "react";

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification("Connected", theme); // always the latest theme
  });

  useEffect(() => {
    const conn = createConnection(roomId);
    conn.on("connected", () => onConnected());
    conn.connect();
    return () => conn.disconnect();
  }, [roomId]); // theme is not a dependency, and onConnected must not be listed
}

よくある間違い:エフェクトイベントを依存配列に入れたり、レンダリング中に呼んだり、ほかのコンポーネントに渡したりすること。Reactのドキュメントは、エフェクトイベントはエフェクト(またはほかのエフェクトイベント)の中からだけ呼ぶよう説明しており、依存として列挙するとlintルールが警告します。React 18での一般的な代替は、このページの後半にあるuseIntervalで使っている「最新のref」パターンです。

パフォーマンスのフック

useMemo

役割:計算の結果をキャッシュし、依存が変わったときだけ再計算します。

const filtered = useMemo(
  () => items.filter((item) => item.includes(query)),
  [items, query],
);

よくある間違い:何でもこれで包むこと。a + bや短いmapのような軽い処理をメモ化しても、コードと比較のコストが増えるだけで得るものはありません。まず測定し(React DevToolsのProfiler)、重い計算、memoされた子に渡す値、エフェクトの依存として使う値をメモ化しましょう。関連する落とし穴として、レンダリングのたびに新しいオブジェクトになる依存({ sort: "asc" })があると、メモは毎回再計算されます。

useCallback

役割:依存が変わるまで、レンダリング間で同じ関数インスタンスを返します。関数をmemoで包んだ子に渡す場合や、エフェクトの依存として使う場合に意味があります。

const handleAdd = useCallback(() => setCount((c) => c + 1), []);

const Child = memo(function Child({ onClick }: { onClick: () => void }) {
  return <button onClick={onClick}>Add</button>;
});

<Child onClick={handleAdd} />

よくある間違い:子がメモ化されていないのにuseCallbackを使うこと。子にmemoがなければ、どのみち親と一緒に再レンダリングされるので、関数を安定させても何も変わりません。ライブデモでは両方の子を表示しており、キャッシュされた関数を受け取る側だけがレンダリングを省きます。もう1つの落とし穴は古いクロージャです。コールバックがcountを読むのに依存リストが空だと、最初の値を見続けます。更新関数を使うか、依存を列挙しましょう。

useTransition

役割:状態の更新を緊急でないものとして扱います。Reactは新しい画面を準備している間も現在の画面の応答性を保ち、読み込み中の表示に使えるisPendingフラグを提供します。

const [isPending, startTransition] = useTransition();

function selectTab(next: string) {
  startTransition(() => {
    setTab(next);
  });
}

よくある間違い:テキスト入力そのものの状態をトランジションで包むこと。制御された入力は緊急に更新されなければならず、そうしないと入力が壊れたように感じられます。入力の状態は通常どおりにし、遅い派生的な更新をトランジションに移しましょう(またはuseDeferredValueを使います)。React 18ではstartTransitionに渡す関数は同期関数でなければなりませんが、React 19では非同期関数も受け付けます。

useDeferredValue

役割:遅れて追従してもよい値のコピーを提供します。UIの速い部分は本来の値を使い、遅い部分は遅延された値で描画して、ブラウザーが空いたときに追いつきます。

const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;

<input value={query} onChange={(e) => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.5 : 1 }}>
  <SlowResults query={deferredQuery} />
</div>

よくある間違い:遅い子をmemoで包み忘れること。どのみち子がキー入力のたびに親と一緒に再レンダリングされるなら、propを遅延させても効果はありません。また、これはデバウンスではありません。決まった時間を待つわけでも、ネットワークリクエストを減らすわけでもありません。リクエストを減らしたいならデバウンスしましょう(下のuseDebounceを参照)。

その他のフック

useId

役割:アプリ内で一意で、サーバーとクライアントで同一になるIDを生成します。htmlFor、aria-describedbyなどの属性に使います。

function Field({ label }: { label: string }) {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </>
  );
}

よくある間違い:リストのkeyを作るのに使うこと。keyはフックからではなく、データ(データベースのIDなど)から作るべきです。また、サーバーレンダリングするアプリでは、要素のIDにMath.random()を使うのは避けましょう。サーバーとブラウザーで異なる数値が生成されてハイドレーションが失敗します。これこそuseIdが解決する問題です。

useSyncExternalStore

役割:Reactが管理していないデータソース(ブラウザーAPI、グローバルなストア)をコンポーネントに購読させ、並行レンダリング中も一貫性が保たれるスナップショットを読みます。

function subscribe(callback: () => void) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}

const online = useSyncExternalStore(
  subscribe,
  () => navigator.onLine, // client snapshot
  () => true,             // server snapshot
);

よくある間違い:subscribeをコンポーネントの中にインラインで定義すること。レンダリングのたびに新しい関数になると、Reactは毎回購読を解除して再購読します。コンポーネントの外で定義するか、useCallbackで包みましょう。また、スナップショット関数は何も変わっていないときに同じ値を返す必要があります。呼ぶたびに新しいオブジェクトや配列を返すと、無限にレンダリングが繰り返されます。

useDebugValue

役割:React DevToolsでカスタムフックにラベルを追加します。利用者に見えるものには影響しません。

function useOnlineStatus() {
  const online = useSyncExternalStore(subscribe, () => navigator.onLine);
  useDebugValue(online ? "Online" : "Offline");
  return online;
}

よくある間違い:すべてのカスタムフックに追加したり、その中で重い整形処理をしたりすること。DevToolsで状態が読み取りにくい共有フックに使い、ラベルを作るのに手間がかかる場合は、2つ目の引数に整形関数を渡しましょう:useDebugValue(date, d => d.toISOString())。

React 19のフック(コードのみ、React 18では実行不可)

React 19のみ。このセクションの4つのフック(use、useActionState、useOptimistic、useFormStatus)は、React 18からはエクスポートされていません。このページ自体はReact 18で動いているので、ライブデモなしで、React 19のプロジェクトにコピーできるコードとして示しています。前述のuseEffectEventにはReact 19.2が必要です。

useActionState

役割:フォームアクションを包み、その最新の結果と保留中のフラグを提供します。アクションは前の状態と、送信されたFormDataを受け取ります。

// React 19+
import { useActionState } from "react";

async function subscribe(prev: { error?: string; ok?: boolean }, formData: FormData) {
  const email = String(formData.get("email") ?? "");
  if (!email.includes("@")) return { error: "Enter a valid email" };
  await saveEmail(email);
  return { ok: true };
}

function Newsletter() {
  const [state, formAction, isPending] = useActionState(subscribe, {});
  return (
    <form action={formAction}>
      <input name="email" type="email" />
      <button disabled={isPending}>{isPending ? "Saving..." : "Subscribe"}</button>
      {state.error && <p role="alert">{state.error}</p>}
      {state.ok && <p>Thanks, you are subscribed.</p>}
    </form>
  );
}

よくある間違い:アクションの1つ目の引数が前の状態であることを忘れること。async function subscribe(formData)と書くと、formDataは実は古い状態になり、.getがエラーを投げます。また、React 19のcanary期間中には、一時的にuseFormState(react-dom内)という名前だった点にも注意してください。現在のドキュメントではreactのuseActionStateを使います。

useOptimistic

役割:非同期のアクションの実行中に、一時的な楽観的な版の状態を表示し、完了したら本来の状態に戻ります。

// React 19+
import { useOptimistic } from "react";

function Thread({ messages, send }: Props) {
  const [optimistic, addOptimistic] = useOptimistic(
    messages,
    (current, text: string) => [...current, { text, sending: true }],
  );

  async function action(formData: FormData) {
    const text = String(formData.get("text"));
    addOptimistic(text);
    await send(text); // parent updates `messages` when this resolves
  }

  return (
    <form action={action}>
      {optimistic.map((m, i) => <p key={i}>{m.text}{m.sending && " (sending)"}</p>)}
      <input name="text" />
    </form>
  );
}

よくある間違い:楽観的なセッターをアクションやトランジションの外で呼ぶこと。ReactはそれがフォームアクションかstartTransitionの中で呼ばれることを想定しています。それ以外の場所では、楽観的な値が現れてはすぐ消えてしまいます(開発時にはReactが警告します)。そして失敗には必ず対処しましょう。sendがエラーを投げたら、メッセージが黙って消えないようにエラーを表示します。

use

役割:Promiseの値(解決するまでサスペンドします)やコンテキストを読みます。ほかのフックと違い、条件分岐やループの中でも呼べます。

// React 19+
import { use, Suspense } from "react";

function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  const comments = use(commentsPromise); // suspends until resolved
  return comments.map((c) => <p key={c.id}>{c.text}</p>);
}

function Page({ commentsPromise }) {
  return (
    <Suspense fallback={<p>Loading comments...</p>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  );
}

// Conditional context read, which useContext cannot do:
function Heading({ show }: { show: boolean }) {
  if (!show) return null;
  const theme = use(ThemeContext);
  return <h1 className={theme}>Hello</h1>;
}

よくある間違い:use(fetch(url))のように、useを呼ぶコンポーネントの中でPromiseを作ること。レンダリングのたびに新しいPromiseができるので、コンポーネントは永遠にサスペンドし続けます。Promiseは1回だけ作り(サーバーコンポーネント、ローダー、キャッシュ、または親の中で)、下へ渡しましょう。また、useはtry/catchの中には置けません。エラーはエラーバウンダリーで処理します。

useFormStatus

役割:react-domからインポートします。親の<form>の状態、つまりpending、data、method、actionを読みます。

// React 19+
import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending } = useFormStatus();
  return <button type="submit" disabled={pending}>{pending ? "Sending..." : "Send"}</button>;
}

function ContactForm() {
  return (
    <form action={sendMessage}>
      <textarea name="message" />
      <SubmitButton />
    </form>
  );
}

よくある間違い:<form>を描画しているのと同じコンポーネントで呼ぶこと。呼び出したコンポーネントの上にあるフォームについてしか報告しないので、フォーム自身のコンポーネントではpendingが常にfalseになります。上の例のように、ボタンを独立したコンポーネントに移しましょう。

依存配列の解説

useEffect、useLayoutEffect、useMemo、useCallback、useImperativeHandleは、最後の引数として値のリストを受け取ります。Reactは各項目を、前回のレンダリング時の値とObject.isで比較します。どれか1つでも変わっていれば、エフェクトは再実行されます(またはメモが再計算されます)。この1つのルールで、すべてのケースが説明できます。

書き方エフェクトが実行されるタイミング典型的な用途
useEffect(fn)毎回のレンダリング後まれ。たいていはレンダリング中に計算すべきものがあるというサイン
useEffect(fn, [])マウント後に1回(開発時のStrict Modeでは2回)、アンマウント時にクリーンアップpropsも状態も使わない購読や初期設定
useEffect(fn, [a, b])マウント後、その後はaかbが変わるたびIDによるデータ取得、propとの同期

ボタンをクリックしてみてください。bを変えると配列なしのエフェクトだけが再実行され、aを変えるとそれと[a]のエフェクトの両方が再実行され、[]のエフェクトはマウント後は静かなままです。

依存のバグをほとんど防ぐ5つのルール

  1. コールバックが読むリアクティブな値はすべて列挙する。props、状態、そしてコンポーネント本体で宣言されたものすべてです。1つでも漏れると古いクロージャになり、コールバックは古いレンダリング時の値を持ち続けます。eslint-plugin-react-hooksをインストールし、exhaustive-depsに足りないものを教えてもらいましょう。
  2. レンダリング中に作られたオブジェクトや関数は毎回新しい。{ id }や() => {}はObject.isのもとでは前回のものと決して等しくならないので、毎回のレンダリングでエフェクトを再実行させます。エフェクトの中か、コンポーネントの外に移すか、メモ化しましょう。
  3. できるだけプリミティブな値に依存する。IDだけが重要な場合、[user.id]は[user]より安定しています。
  4. 更新関数を使って状態への依存をなくす。setCount(c => c + 1)はcountを読まないので、countを列挙する必要はありません。
  5. ループを「直す」ためにリンターを黙らせない。無限ループはたいてい、エフェクトが依存でもある状態を設定しているか、新しいオブジェクトに依存していることを意味します。原因を直しましょう。反応せずに読みたいだけの値には、React 19.2以降ならuseEffectEventを、React 18なら常に最新に保つrefを使います。
// Stale closure: logs 0 forever
useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);
  return () => clearInterval(id);
}, []); // count is missing

// Fixed: re-subscribe when count changes
useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);
  return () => clearInterval(id);
}, [count]);

// Fetch with cleanup so a slow old response cannot overwrite a newer one
useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/users/${userId}`, { signal: controller.signal })
    .then((r) => r.json())
    .then(setUser)
    .catch((err) => { if (err.name !== "AbortError") setError(err); });
  return () => controller.abort();
}, [userId]);

プロジェクトにコピーして使える12のカスタムフック

カスタムフックとは、名前がuseで始まり、ほかのフックを呼ぶ関数です。複数のコンポーネントで状態を持つロジックを共有でき、呼び出したコンポーネントはそれぞれ独立した状態を持ちます。以下のフックはこのページのためにTypeScriptで書いたもので、React 18と19で動作し、依存パッケージはありません。それぞれの最後に、使うべきでない場面についての補足があります。

1. useDebounce

入力がdelayミリ秒のあいだ変わらなくなってから更新される値を返します。APIを呼ぶ検索ボックスに向いています。

import { useEffect, useState } from "react";

export function useDebounce<T>(value: T, delay = 300): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = window.setTimeout(() => setDebounced(value), delay);
    return () => window.clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

// const debouncedQuery = useDebounce(query, 400);
// useEffect(() => { search(debouncedQuery); }, [debouncedQuery]);

使うべきでない場面:大きなリストを描画する間、UIの応答性を保ちたいだけの場合。それは描画の問題で、useDeferredValueなら人為的な遅延なしに解決できます。また、即座に反応すべきフォームの検証にも使わないでください。

2. useLocalStorage

useStateのように動きますが、値をlocalStorageに保存し、次回の訪問時に復元します。

import { useEffect, useState } from "react";

export function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const raw = window.localStorage.getItem(key);
      return raw === null ? initial : (JSON.parse(raw) as T);
    } catch {
      return initial; // private mode, blocked storage or bad JSON
    }
  });

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch {
      /* storage full or blocked: keep the in-memory value */
    }
  }, [key, value]);

  return [value, setValue] as const;
}

使うべきでない場面:機密性のあるもの(トークン、個人データ)。ページ上のどのスクリプトでもlocalStorageを読めるからです。また、サーバーレンダリングするページで無造作に使うのも避けましょう。サーバーにはwindowがないので、ハイドレーションの不一致を避けるため、マウント後にストレージを読みます。タブ間では同期しないので、必要ならstorageイベントのリスナーを追加してください。

3. useMediaQuery

CSSのメディアクエリが一致している間はtrueを返し、変化すると更新されます。

import { useCallback, useSyncExternalStore } from "react";

export function useMediaQuery(query: string): boolean {
  const subscribe = useCallback((onChange: () => void) => {
    const mql = window.matchMedia(query);
    mql.addEventListener("change", onChange);
    return () => mql.removeEventListener("change", onChange);
  }, [query]);

  return useSyncExternalStore(
    subscribe,
    () => window.matchMedia(query).matches,
    () => false, // server snapshot
  );
}

// const isDesktop = useMediaQuery("(min-width: 1024px)");
// const reduceMotion = useMediaQuery("(prefers-reduced-motion: reduce)");

使うべきでない場面:CSSだけで実現できるレイアウトの変更。Tailwindのブレークポイント(md:hidden)で要素を隠したり並べ替えたりすれば、JavaScriptの読み込み前から動き、ちらつくこともありません。別々のコンポーネントを描画しなければならない場合や、何かの読み込み自体を省きたい場合にこのフックを使います。

4. useToggle

安定したtoggle関数を持つ真偽値です。メニュー、モーダル、開閉セクションに使います。

import { useCallback, useState } from "react";

export function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn((v) => !v), []);
  return [on, toggle, setOn] as const;
}

// const [open, toggleOpen, setOpen] = useToggle();

使うべきでない場面:状態が2つより多い値(閉、開き中、開、エラー)を持つ場合。いくつものトグルを重ねるのではなく、useStateやuseReducerで文字列のユニオン型を使いましょう。

5. usePrevious

前回のレンダリング時の値を返します。変化の表示(「12から増加」)や、遷移への反応に便利です。

import { useEffect, useRef } from "react";

export function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

使うべきでない場面:状態を更新するかどうかの判断。Reactのドキュメントには、そのためのよりきれいなパターンが示されています。前の値を状態に保存し、レンダリング中に比較する方法です。このフックはレンダリング中にrefを読んでおり、今は動きますが、Reactの指針がなるべく少なくするよう求めている種類のコードです。

6. useOnClickOutside

指定した要素の外側のどこかを利用者が押したときに、ハンドラーを呼びます。自作のメニューやポップオーバーを閉じるのによく使います。

import { useEffect, useRef, type RefObject } from "react";

export function useOnClickOutside(
  ref: RefObject<HTMLElement>,
  handler: (event: PointerEvent) => void,
) {
  const saved = useRef(handler);
  useEffect(() => { saved.current = handler; }, [handler]);

  useEffect(() => {
    const listener = (event: PointerEvent) => {
      if (!ref.current || ref.current.contains(event.target as Node)) return;
      saved.current(event);
    };
    document.addEventListener("pointerdown", listener);
    return () => document.removeEventListener("pointerdown", listener);
  }, [ref]);
}

使うべきでない場面:メニューやダイアログにすでにヘッドレスライブラリ(Radix、Headless UI)を使っている場合。それらは外側のクリック、Escape、フォーカスの返却を処理してくれます。このフックだけで作った手製のメニューは、たいていキーボード操作への対応が抜けています。

7. useInterval

常に最新のコールバックを呼び、遅延にnullを渡すと一時停止できるsetIntervalです。

import { useEffect, useRef } from "react";

export function useInterval(callback: () => void, delay: number | null) {
  const saved = useRef(callback);
  useEffect(() => { saved.current = callback; }, [callback]);

  useEffect(() => {
    if (delay === null) return;
    const id = window.setInterval(() => saved.current(), delay);
    return () => window.clearInterval(id);
  }, [delay]);
}

// useInterval(() => setTicks((t) => t + 1), running ? 1000 : null);

使うべきでない場面:アニメーション(CSSかrequestAnimationFrameを使いましょう)や、正確な時計。インターバルはずれていき、ブラウザーは背景のタブでその実行を遅くします。カウントダウンでは、毎回の実行時に固定の終了時刻から残り時間を計算しましょう。

8. useWindowSize

現在のウィンドウの幅と高さを返し、リサイズ時に更新されます。

import { useSyncExternalStore } from "react";

function subscribe(onChange: () => void) {
  window.addEventListener("resize", onChange);
  return () => window.removeEventListener("resize", onChange);
}

export function useWindowSize() {
  const width = useSyncExternalStore(subscribe, () => window.innerWidth, () => 0);
  const height = useSyncExternalStore(subscribe, () => window.innerHeight, () => 0);
  return { width, height };
}

使うべきでない場面:ブレークポイント。useMediaQueryはクエリの結果が切り替わったときだけ再レンダリングしますが、こちらはリサイズの1ピクセルごとに再レンダリングします。ウィンドウではなく1つの要素のサイズには、ResizeObserverを使いましょう。(スナップショットを2つに分けているのは意図的です。1つのスナップショット関数から新しいオブジェクトを返すと、永遠にループしてしまいます。)

9. useCopyToClipboard

Clipboard APIでテキストをコピーし、ボタンでの反応表示に使える短時間だけのcopiedフラグを公開します。

import { useCallback, useState } from "react";

export function useCopyToClipboard(resetAfter = 1500) {
  const [copied, setCopied] = useState(false);
  const copy = useCallback(async (text: string) => {
    try {
      await navigator.clipboard.writeText(text);
      setCopied(true);
      window.setTimeout(() => setCopied(false), resetAfter);
      return true;
    } catch {
      setCopied(false); // insecure context or permission denied
      return false;
    }
  }, [resetAfter]);
  return [copied, copy] as const;
}

使うべきでない場面:素のHTTPで配信されるページ。navigator.clipboardには安全なコンテキスト(HTTPSかlocalhost)が必要です。falseが返ったときは、テキストを選択状態にするなどの代替手段を必ず示しましょう。

10. useFetch

URLのJSONを読み込み、読み込み中とエラーの状態を追跡し、URLが変わるかコンポーネントがアンマウントされるとリクエストを中止します。

import { useEffect, useState } from "react";

type FetchState<T> = { data: T | null; error: Error | null; loading: boolean };

export function useFetch<T>(url: string | null): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({ data: null, error: null, loading: !!url });

  useEffect(() => {
    if (!url) return;
    const controller = new AbortController();
    setState((s) => ({ ...s, loading: true, error: null }));

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`Request failed with status ${res.status}`);
        return res.json() as Promise<T>;
      })
      .then((data) => setState({ data, error: null, loading: false }))
      .catch((error: Error) => {
        if (error.name === "AbortError") return; // superseded or unmounted
        setState({ data: null, error, loading: false });
      });

    return () => controller.abort();
  }, [url]);

  return state;
}

使うべきでない場面:大規模なアプリ。キャッシュも、重複の排除も、再試行も、再検証もないので、2つのコンポーネントが同じURLを求めると2回リクエストが送られます。そのどれかが必要になったら、フレームワークのデータ読み込み機能か、データライブラリ(TanStack Query、SWR)を使いましょう。Reactの公式ドキュメントも、エフェクトでのデータ取得について同じことを勧めています。

11. useEventListener

ウィンドウ(または任意の要素)にイベントリスナーを付け、レンダリングのたびに購読し直すことなく、常に最新のハンドラーを呼びます。

import { useEffect, useRef } from "react";

export function useEventListener<K extends keyof WindowEventMap>(
  type: K,
  handler: (event: WindowEventMap[K]) => void,
  target: Window | HTMLElement | null = typeof window === "undefined" ? null : window,
) {
  const saved = useRef(handler);
  useEffect(() => { saved.current = handler; }, [handler]);

  useEffect(() => {
    if (!target) return;
    const listener = (event: Event) => saved.current(event as WindowEventMap[K]);
    target.addEventListener(type, listener);
    return () => target.removeEventListener(type, listener);
  }, [type, target]);
}

// useEventListener("keydown", (e) => { if (e.key === "Escape") close(); });

使うべきでない場面:自分が描画する要素でReactがすでに扱っているイベント。JSXにonClick、onKeyDownなどを直接使いましょう。このフックを使うのは、window、document、サードパーティの要素の場合だけにします。

12. useIsMounted

コンポーネントがまだ画面上にあるかどうかを教えてくれる関数を返します。awaitの後に状態を設定する、古い非同期コード向けです。

import { useCallback, useEffect, useRef } from "react";

export function useIsMounted() {
  const mounted = useRef(false);
  useEffect(() => {
    mounted.current = true;
    return () => { mounted.current = false; };
  }, []);
  return useCallback(() => mounted.current, []);
}

// const isMounted = useIsMounted();
// const data = await load(); if (isMounted()) setData(data);

使うべきでない場面:既定の手段として。React 18では、アンマウントされたコンポーネントの状態設定に関する警告が削除されました。より良い直し方は、処理そのものをキャンセルするか(useFetchのようにAbortControllerを使う)、エフェクトのクリーンアップ内のフラグで古い結果を無視することです。これは、まだ構造を変えられない古いコードのために取っておきましょう。

どのフックが必要?すばやく決めるための一覧

個別のフックではなく、コンポーネントライブラリを作っていますか?React UIライブラリ比較では、同じ6つのコンポーネントをshadcn/ui、Radix、Headless UI、daisyUI、Flowbite、MUIで示しています。Tailwindチートシートでは、このページのデモで使っているユーティリティクラスを解説しています。ReactとTailwindで作った既製のパーツはコンポーネントギャラリーにあります。

よくある質問

Reactには組み込みフックがいくつありますか?

執筆時点(2026年10月、React 19.3)で、Reactのフックリファレンスには、React本体の17個のフックが載っています。useState、useReducer、useContext、useRef、useImperativeHandle、useEffect、useLayoutEffect、useInsertionEffect、useEffectEvent、useMemo、useCallback、useTransition、useDeferredValue、useDebugValue、useId、useSyncExternalStore、useActionStateです。React 19ではさらにuseOptimisticとuse APIが加わり、React DOMにはuseFormStatusがあります。

useMemoとuseCallbackの違いは何ですか?

useMemoは関数を呼んだ結果をキャッシュし、useCallbackは関数そのものをキャッシュします。useCallback(fn, deps)はuseMemo(() => fn, deps)と同等です。重い値にはuseMemoを、メモ化された子に渡す関数やエフェクトの依存として使う関数にはuseCallbackを使いましょう。

useEffectが2回実行されるのはなぜですか?

Strict Modeを有効にした開発環境では、クリーンアップの抜けたエフェクトを見つけやすくするために、Reactが各コンポーネントをマウントし、アンマウントし、もう一度マウントします。本番ビルドでは起きません。2回の実行でバグが起きるなら、エフェクトが始めたことを元に戻すクリーンアップ関数を追加しましょう。

useEffectの空の依存配列は何を意味しますか?

空の配列は、エフェクトがpropsも状態も読まないことをReactに伝えます。そのため、コンポーネントのマウント後に1回だけ実行され、アンマウント時にクリーンアップが実行されます。エフェクトが実際にpropsや状態を読む場合は、それらを列挙しましょう。そうしないと、最初のレンダリング時の値を使い続けます。

useやuseActionStateのようなReact 19のフックをReact 18で使えますか?

使えません。use、useActionState、useOptimistic、useFormStatusはReact 18からはエクスポートされておらず、useEffectEventにはReact 19.2以降が必要です。React 18では、フォームについてはuseStateとイベントハンドラーで、useEffectEventの代わりには最新のコールバックを保持するrefで、似た動作を実現できます。

関数がカスタムフックになる条件は何ですか?

名前がuseで始まり、少なくとも1つのほかのフックを呼んでいることです。この命名によって、Reactのlintルールがフックのルールに従っているかをチェックできます。カスタムフックを呼ぶコンポーネントは、それぞれ別々の状態を持ちます。フックが共有するのはロジックであって、データではありません。

ライブ:{title}

+3(更新関数)

リセット

useReducerのセクションを読む

自分の項目を追加する

新しい項目

新しいToDo項目

完了済みを削除

コンテキストから「{theme}」を読みました

プロバイダーの値を切り替える

ボタンをクリックするとここにフォーカスします

フォーカスの対象

入力欄にフォーカス

refに加算(再レンダリングなし)

再レンダリング

前回のレンダリング時に表示されたrefの値:{n}

ここに入力

子の入力欄

親がshake()を呼ぶ

親がclear()を呼ぶ

エフェクト実行:インターバル開始

クリーンアップ実行:インターバル解除

一時停止

再開

入力してサイズを変えてください

測る対象のテキスト

描画前に測定:幅{n}px

枠線の色

このルールはページのheadに挿入されました

4,000件の項目を絞り込む

項目を絞り込む

無関係な再レンダリング({n})

{n}件一致。絞り込みは{runs}回実行されました(無関係な再レンダリングでは実行されません)。

{label}(子のレンダリング{n}回)

インライン関数

親を再レンダリング

カウント:{n}

概要

遅いリスト

お問い合わせ

切り替え中(前のタブはクリックできるまま)...

表示中:{tab}

行

{tab}タブの内容です。

描画された結果:「{query}」

速く入力してみてください:入力欄は反応したままです

検索

名前

メールアドレス

ネットワーク:

ウィンドウの幅:

ウィンドウのサイズを変えるか、ネットワークをオフにすると更新されます。

このコンポーネントは、React DevToolsで自身に「{label}」というラベルを付けるカスタムフックを呼んでいます。Componentsタブを開くと、フックの横にラベルが表示されます。

オンライン

オフライン

依存配列

[配列なし]が実行(レンダリング{n}回目)

[]がマウント時に1回実行

[a]が実行、a = {n}

aを変更({n})

bを変更({n})

ログを消去

入力して、0.5秒止めてください

デバウンスの入力欄

即時:「{text}」 · デバウンス後:「{debounced}」

入力してから、ページを再読み込みしてください

保存された名前

このブラウザーにだけ保存されます。

このブラウザーにだけ保存されています:「{name}」。

useMediaQuery:ビューポートは{size}、システムのテーマは{theme}です。

768px以上

768px未満

オン

オフ

カウント{n}

前の値は{value}

まだありません

停止

開始

{n}ティック

コピーしました

コマンドをコピー

メニューを開く

メニューの外側のどこかをクリックすると閉じます。

無料で始める · クレジットカード不要

関連: ガイド|Webマーケティングと集客を学ぶ

製品

ご利用いただける方

機能

リソース

会社情報

サイトマップを見る