React Hooks コピペで使える実践 Tips 集【中級者向け】

React Hooks コピペして即使えるコードを中心に、現場で役立つテクニックをまとめました。カスタムフックの実装から React 19 の新 API まで、中級者が「もっと早く知りたかった」と感じる Tips を厳選しています。

コピペで使えるカスタムフック集

useDebounce — 入力遅延処理

import { useState, useEffect } from "react";

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// 使い方
function SearchInput() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      fetch(`/api/search?q=${debouncedQuery}`);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

useLocalStorage — 永続化ステート

import { useState, useCallback } from "react";

function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = useCallback(
    (value: T | ((prev: T) => T)) => {
      setStoredValue((prev) => {
        const next = value instanceof Function ? value(prev) : value;
        try {
          window.localStorage.setItem(key, JSON.stringify(next));
        } catch {
          console.warn(`useLocalStorage: "${key}" への書き込みに失敗しました`);
        }
        return next;
      });
    },
    [key]
  );

  return [storedValue, setValue] as const;
}

useMediaQuery — レスポンシブ対応

import { useState, useEffect } from "react";

function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(
    () => window.matchMedia(query).matches
  );

  useEffect(() => {
    const mql = window.matchMedia(query);
    const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
    mql.addEventListener("change", handler);
    return () => mql.removeEventListener("change", handler);
  }, [query]);

  return matches;
}

// 使い方
function ResponsiveNav() {
  const isMobile = useMediaQuery("(max-width: 768px)");
  return isMobile ? <MobileNav /> : <DesktopNav />;
}

usePrevious — 直前の値を保持

import { useRef, useEffect } from "react";

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

// 使い方
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return (
    <p>
      現在: {count} / 直前: {prevCount ?? "なし"}
    </p>
  );
}

useSyncExternalStore の活用

React 18 で導入された useSyncExternalStore は、ブラウザ API や外部ストアをサブスクライブするためのフックです。useEffect + useState のパターンより安全にサーバーサイドレンダリングに対応できます。

ウィンドウサイズの購読

import { useSyncExternalStore } from "react";

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

function getSnapshot() {
  return window.innerWidth;
}

function getServerSnapshot() {
  return 0;
}

function useWindowWidth() {
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}

オンライン状態の購読

import { useSyncExternalStore } from "react";

function useOnlineStatus() {
  return useSyncExternalStore(
    (callback) => {
      window.addEventListener("online", callback);
      window.addEventListener("offline", callback);
      return () => {
        window.removeEventListener("online", callback);
        window.removeEventListener("offline", callback);
      };
    },
    () => navigator.onLine,
    () => true
  );
}

function StatusBadge() {
  const isOnline = useOnlineStatus();
  return <span>{isOnline ? "オンライン" : "オフライン"}</span>;
}

useOptimistic で楽観的 UI 更新(React 19)

React 19 の useOptimistic を使うと、サーバーのレスポンスを待たずに UI を先行更新し、失敗時に自動でロールバックできます。

import { useOptimistic, useTransition } from "react";

type Message = { id: string; text: string; pending?: boolean };

function MessageList({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state: Message[], newText: string) => [
      ...state,
      { id: crypto.randomUUID(), text: newText, pending: true },
    ]
  );
  const [isPending, startTransition] = useTransition();

  async function handleSubmit(formData: FormData) {
    const text = formData.get("text") as string;
    startTransition(async () => {
      addOptimisticMessage(text);
      await fetch("/api/messages", {
        method: "POST",
        body: JSON.stringify({ text }),
        headers: { "Content-Type": "application/json" },
      });
    });
  }

  return (
    <>
      <ul>
        {optimisticMessages.map((msg) => (
          <li key={msg.id} style={{ opacity: msg.pending ? 0.5 : 1 }}>
            {msg.text}
          </li>
        ))}
      </ul>
      <form action={handleSubmit}>
        <input name="text" required />
        <button disabled={isPending}>送信</button>
      </form>
    </>
  );
}

useCallback / useMemo の正しい使いどころ

useCallback が有効なケース

子コンポーネントが React.memo でラップされており、かつコールバックを props として受け取る場合に効果があります。

import { useCallback, memo } from "react";

const ExpensiveList = memo(function ExpensiveList({
  items,
  onDelete,
}: {
  items: string[];
  onDelete: (id: string) => void;
}) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item}>
          {item}
          <button onClick={() => onDelete(item)}>削除</button>
        </li>
      ))}
    </ul>
  );
});

function Parent({ items }: { items: string[] }) {
  const [list, setList] = useState(items);

  const handleDelete = useCallback((id: string) => {
    setList((prev) => prev.filter((item) => item !== id));
  }, []);

  return <ExpensiveList items={list} onDelete={handleDelete} />;
}

useMemo が有効なケース

計算コストが高い処理や、参照同一性を保ちたいオブジェクト・配列に使います。

import { useMemo } from "react";

function useFilteredAndSorted<T extends Record<string, unknown>>(
  data: T[],
  filterFn: (item: T) => boolean,
  sortKey: keyof T
) {
  return useMemo(
    () =>
      data
        .filter(filterFn)
        .sort((a, b) => (a[sortKey] > b[sortKey] ? 1 : -1)),
    [data, filterFn, sortKey]
  );
}

単純な値の計算や、毎回再生成しても安いプリミティブには不要です。「パフォーマンス問題が計測で確認された」タイミングで追加するのが原則です。

useRef でフォームと DOM 操作

フォーカス制御

import { useRef, useEffect } from "react";

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} placeholder="自動フォーカス" />;
}

非制御フォームの値を取得

import { useRef } from "react";

function UncontrolledForm() {
  const nameRef = useRef<HTMLInputElement>(null);
  const emailRef = useRef<HTMLInputElement>(null);

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    const payload = {
      name: nameRef.current!.value,
      email: emailRef.current!.value,
    };
    console.log(payload);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input ref={nameRef} name="name" />
      <input ref={emailRef} name="email" type="email" />
      <button type="submit">送信</button>
    </form>
  );
}

再レンダーをトリガーしない値の保持

import { useRef, useEffect } from "react";

function useTimer() {
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

  function start(callback: () => void, ms: number) {
    intervalRef.current = setInterval(callback, ms);
  }

  function stop() {
    if (intervalRef.current !== null) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  }

  useEffect(() => () => stop(), []);

  return { start, stop };
}

useReducer で複雑な状態管理

複数のステートが連動して変化する場合は useReducer で一元管理するとバグが減ります。

import { useReducer } from "react";

type Status = "idle" | "loading" | "success" | "error";

type State<T> = {
  status: Status;
  data: T | null;
  error: string | null;
};

type Action<T> =
  | { type: "FETCH_START" }
  | { type: "FETCH_SUCCESS"; payload: T }
  | { type: "FETCH_ERROR"; payload: string }
  | { type: "RESET" };

function createReducer<T>() {
  return function reducer(state: State<T>, action: Action<T>): State<T> {
    switch (action.type) {
      case "FETCH_START":
        return { status: "loading", data: null, error: null };
      case "FETCH_SUCCESS":
        return { status: "success", data: action.payload, error: null };
      case "FETCH_ERROR":
        return { status: "error", data: null, error: action.payload };
      case "RESET":
        return { status: "idle", data: null, error: null };
    }
  };
}

function useFetch<T>(url: string) {
  const [state, dispatch] = useReducer(createReducer<T>(), {
    status: "idle",
    data: null,
    error: null,
  });

  async function execute() {
    dispatch({ type: "FETCH_START" });
    try {
      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = (await res.json()) as T;
      dispatch({ type: "FETCH_SUCCESS", payload: data });
    } catch (e) {
      dispatch({
        type: "FETCH_ERROR",
        payload: e instanceof Error ? e.message : "不明なエラー",
      });
    }
  }

  return { ...state, execute, reset: () => dispatch({ type: "RESET" }) };
}

useReducer はロジックをコンポーネント外に分離できるため、テストも書きやすくなります。

まとめ

今回紹介した Tips を一覧で振り返ります。

フック / API用途使いどころ
useDebounce入力遅延検索・オートコンプリート
useLocalStorage永続ステートテーマ設定・フォーム下書き
useMediaQueryブレークポイント検知レスポンシブ UI 分岐
usePrevious直前の値保持アニメーション・差分表示
useSyncExternalStore外部ストア購読ブラウザ API・カスタムストア
useOptimistic楽観的 UI 更新いいね・送信フォーム
useCallbackコールバック安定化memo 子コンポーネントへの props
useMemo計算結果キャッシュ重い計算・参照同一性が必要な値
useRefDOM 参照 / 非レンダー値フォーカス制御・タイマー管理
useReducer複雑な状態遷移フェッチ状態・マルチステップフォーム

カスタムフックはロジックを再利用可能な単位に切り出すことで、コンポーネントをシンプルに保てます。「いつ使うか」の判断基準を身につけることが、React 中級から上級へのステップです。