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 | 計算結果キャッシュ | 重い計算・参照同一性が必要な値 |
useRef | DOM 参照 / 非レンダー値 | フォーカス制御・タイマー管理 |
useReducer | 複雑な状態遷移 | フェッチ状態・マルチステップフォーム |
カスタムフックはロジックを再利用可能な単位に切り出すことで、コンポーネントをシンプルに保てます。「いつ使うか」の判断基準を身につけることが、React 中級から上級へのステップです。