ニッチワークス合同会社

← ブログ一覧

React Hooks 完全ガイド:いつ使うか・使わないと何が起きるか

2026-07-03

#React#フロントエンド#JavaScript#パフォーマンス


useMemo って全部につければいいんですよね?」「useCallback 入れたのに子が再レンダーされる……」——React を書いていると一度は当たる疑問と詰まりポイントを、バグ例と修正コードを並べながら整理する。

Hook の使い所チートシート

Hook 何をするか 使わないと
useState レンダーをトリガーする可変値 値が変わっても画面が更新されない
useEffect 副作用の実行とクリーンアップ リーク・無限ループ・古い値の参照
useMemo 重い計算結果・値参照のキャッシュ 毎レンダーで再計算される
useCallback 関数参照の安定化 React.memo の子が毎回再レンダー
useRef レンダーをまたぐ可変値・DOM参照 再レンダーで値が消える、または不要な再レンダーが起きる
useContext コンポーネントツリー越しの値共有 props リレーが深くなる

useState

基本

useState は「変更したら再レンダーをトリガーする値」を持つためのフック。setter を呼ばないと React は変化に気づかない。

const [count, setCount] = useState(0);

バグ①:配列・オブジェクトを直接変更する

// BAD: 同じ参照を渡しても React は変化を検知しない
const [items, setItems] = useState([1, 2, 3]);

function addItem() {
  items.push(4); // 配列を直接変更
  setItems(items); // 同じ参照を渡している → 再レンダーしない
}
// GOOD: 新しい配列・オブジェクトを返す
setItems([...items, 4]);
setItems(prev => [...prev, 4]); // 前の値に依存するときはこちら

バグ②:前の state に依存した更新

// BAD: 連続で呼ぶと意図した値にならないことがある
setCount(count + 1);
setCount(count + 1); // count はどちらも古い値を読んでいる
// GOOD: 前の値を受け取る関数形式
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 確実に +2 になる

useEffect

useEffect は「render の外側で起きること」を扱う。fetch、イベント登録、タイマー、外部ライブラリとの連携など。

Mount(初回) Render コンポーネント関数が実行される DOM 更新 画面に変更が反映される Effect 実行 fetch・addEventListener など return でクリーンアップ関数を登録 Update(deps が変化) Render props / state 変化で再実行 DOM 更新 画面に変更が反映される 前の Effect をクリーンアップ ← ここが重要。新しい Effect の前に必ず実行 前の fetch キャンセル・listener 削除 など 新しい Effect 実行 新しい deps の値で動く deps が変わるたびに ③④ が繰り返される Unmount(削除時) (Render なし) (DOM 更新なし) 最後のクリーンアップ コンポーネントが消える直前に実行 timer・listener・fetch をすべて解除 Effect(副作用) Cleanup(後片付け) Render

基本構造

useEffect(() => {
  // Effect 本体:DOM 更新後に実行

  return () => {
    // クリーンアップ:次の Effect 実行前・アンマウント時に実行
  };
}, [/* 依存配列 */]);

バグ①:依存配列にオブジェクトを入れる(無限ループ)

// BAD: options は毎レンダーで新しいオブジェクト → Effect が毎回動く
function UserList({ page }) {
  const options = { page, limit: 20 }; // 毎レンダーで新参照

  useEffect(() => {
    fetch('/api/users', { body: JSON.stringify(options) })
      .then(r => r.json())
      .then(setUsers);
  }, [options]); // options が変わり続ける → 無限ループ
}
// GOOD: プリミティブを依存配列に入れる
useEffect(() => {
  fetch(`/api/users?page=${page}&limit=20`)
    .then(r => r.json())
    .then(setUsers);
}, [page]); // page という数値だけ見る

バグ②:クリーンアップなしのリーク

コンポーネントがアンマウントされても fetch は走り続ける。アンマウント後に setState が呼ばれると React 警告(またはバグ)になる。

// BAD: アンマウントされても fetch が続く
useEffect(() => {
  fetch('/api/data').then(r => r.json()).then(setData);
}, []);
// GOOD: AbortController でキャンセル
useEffect(() => {
  const controller = new AbortController();

  fetch('/api/data', { signal: controller.signal })
    .then(r => r.json())
    .then(setData)
    .catch(e => {
      if (e.name !== 'AbortError') throw e;
    });

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

バグ③:依存配列の省略

// BAD: count を読んでいるのに依存配列が空 → 古い count を参照し続ける
useEffect(() => {
  const id = setInterval(() => {
    console.log(count); // 常に初期値(0)が出る
  }, 1000);
  return () => clearInterval(id);
}, []); // count が変わっても Effect は再実行されない
// GOOD: count を依存配列に入れる
useEffect(() => {
  const id = setInterval(() => {
    console.log(count);
  }, 1000);
  return () => clearInterval(id);
}, [count]);

// または useRef で最新値を参照する(Effect の再実行を避けたい場合)
const countRef = useRef(count);
useEffect(() => { countRef.current = count; }, [count]);

useEffect(() => {
  const id = setInterval(() => {
    console.log(countRef.current); // 常に最新値
  }, 1000);
  return () => clearInterval(id);
}, []); // インターバルは一度だけ作る

useMemo

「再レンダーのたびに重い計算を走らせたくない」または「値の参照を安定させたい」ときに使う。

ケース①:重い計算のキャッシュ

// BAD: 10万件のフィルタが毎レンダーで実行される
function ProductList({ products, search }) {
  const filtered = products.filter(p =>
    p.name.toLowerCase().includes(search.toLowerCase())
  );

  return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
// GOOD: products か search が変わったときだけ再計算
function ProductList({ products, search }) {
  const filtered = useMemo(
    () => products.filter(p =>
      p.name.toLowerCase().includes(search.toLowerCase())
    ),
    [products, search]
  );

  return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

ケース②:参照を安定させて useEffect の暴走を防ぐ

// BAD: config は毎レンダーで新しいオブジェクト → useEffect が毎回動く
function Chart({ data }) {
  const config = { color: 'steelblue', width: 600 }; // 毎レンダーで新参照

  useEffect(() => {
    renderChart(data, config);
  }, [data, config]); // config が変わり続ける → 毎回実行
}
// GOOD: 依存が変わらない限り同じ参照
function Chart({ data }) {
  const config = useMemo(
    () => ({ color: 'steelblue', width: 600 }),
    [] // 変わらないので空
  );

  useEffect(() => {
    renderChart(data, config);
  }, [data, config]); // data が変わったときだけ実行
}

いつ useMemo は不要か

  • 単純な計算(文字列結合、short array map など)
  • レンダー頻度が低いコンポーネント
  • メモ化のコスト(比較処理)が計算コストより高いケース

「とりあえず全部 useMemo」はオーバーエンジニアリング。重い計算や参照が問題になってから入れる。


useCallback

関数参照を安定させる。useMemo(() => fn, deps) の関数版。React.memo と組み合わせて初めて効果がある。

なぜ useCallback が必要か

useCallback なし count ボタン押下(count + 1) Parent 再レンダー handleClick = 新しい関数参照 (毎レンダーで関数が作り直される) Child 再レンダー ✗ props(関数)が変わったと判断 React.memo があっても通過してしまう React.memo の最適化が無効になる useCallback + React.memo count ボタン押下(count + 1) Parent 再レンダー handleClick = 同じ参照(キャッシュ) (useCallback が依存変化まで保持) React.memo チェック props 変化なし → Child をスキップ ✓ 関数参照が同じため再レンダー不要と判断 Child の再レンダーなし

// BAD: handleClick は毎レンダーで新しい関数参照になる
function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log('clicked');
  };

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>カウント: {count}</button>
      <Child onClick={handleClick} />
    </>
  );
}

// React.memo で囲んでいても、毎回 handleClick が新参照なので再レンダーされる
const Child = React.memo(function Child({ onClick }) {
  console.log('Child レンダー');
  return <button onClick={onClick}>子ボタン</button>;
});
// GOOD: useCallback で参照を安定させる
const handleClick = useCallback(() => {
  console.log('clicked');
}, []); // 依存なし = 同じ関数を使い続ける

これで count が変わっても handleClick の参照は変わらず、Child はスキップされる。

セットで必要なもの

組み合わせ 結果
useCallback だけ 参照は安定するが子は React.memo なしなので毎回レンダー
React.memo だけ 毎レンダーで新しい関数参照が渡る → props 変化と判断 → 再レンダー
両方 参照が安定 → props 変化なし → 子をスキップ

state を参照する場合

// BAD: count を読んでいるのに依存配列が空 → 古い count を参照し続ける
const handleSave = useCallback(() => {
  saveToServer(count); // 常に初期値
}, []);
// GOOD: 依存配列に count を入れる(count が変わると関数が作り直される)
const handleSave = useCallback(() => {
  saveToServer(count);
}, [count]);

useRef

再レンダーをトリガーせずに値を保持する。DOM 操作にも使う。

ケース①:DOM 参照

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

  useEffect(() => {
    inputRef.current?.focus(); // マウント後にフォーカス
  }, []);

  return <input ref={inputRef} />;
}

ケース②:インターバル ID などを保持する

// BAD: useState で interval ID を持つと setInterval のたびに再レンダーが起きる
const [intervalId, setIntervalId] = useState<number | null>(null);
// GOOD: useRef は値が変わっても再レンダーしない
function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef<number | null>(null);

  const start = () => {
    intervalRef.current = window.setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  };

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

  return (
    <>
      <p>{count}</p>
      <button onClick={start}>開始</button>
      <button onClick={stop}>停止</button>
    </>
  );
}

ケース③:前の値を保持するカスタムフック

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined);
  useEffect(() => {
    ref.current = value; // render 後に更新
  });
  return ref.current; // render 時点では前の値
}

function Counter() {
  const [count, setCount] = useState(0);
  const prev = usePrevious(count);

  return (
    <p>
      {prev ?? '-'} → {count}
    </p>
  );
}

useContext

コンポーネントツリーを跨いで値を共有する。深い props リレーを避けるために使う。

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

function App() {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  return (
    <ThemeContext.Provider value={theme}>
      <Layout />
    </ThemeContext.Provider>
  );
}

function Button() {
  const theme = useContext(ThemeContext); // どの階層からでも読める
  return <button className={theme}>クリック</button>;
}

注意点:context が変わると全 consumer が再レンダーされる

値が頻繁に変わるものと、そうでないものを同じ context に混ぜると余計な再レンダーが増える。頻度ごとに context を分割する。

// 分けるべき例
const UserContext = createContext<User | null>(null);        // 頻繁には変わらない
const NotificationContext = createContext<number>(0);        // 頻繁に変わる

// 通知数が変わっても User を読んでいるコンポーネントは再レンダーされない

カスタム Hook で再利用する

複数コンポーネントで同じロジックが必要なら、カスタム Hook に切り出す。use で始まる関数の中では他の Hook を呼べる。

function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);

    fetch(url, { signal: controller.signal })
      .then(r => {
        if (!r.ok) throw new Error(`${r.status}`);
        return r.json();
      })
      .then((d: T) => {
        setData(d);
        setLoading(false);
      })
      .catch(e => {
        if (e.name !== 'AbortError') {
          setError(e);
          setLoading(false);
        }
      });

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

  return { data, loading, error };
}

// 使う側
function UserProfile({ id }: { id: string }) {
  const { data, loading, error } = useFetch<User>(`/api/users/${id}`);

  if (loading) return <p>読み込み中…</p>;
  if (error) return <p>エラー: {error.message}</p>;
  return <p>{data?.name}</p>;
}

まとめ:判断フロー

値を持ちたい
├─ 変わったら再レンダーが必要  → useState
└─ 再レンダーは不要(ID・ref・前の値)  → useRef

fetch・イベント登録・タイマーを動かしたい  → useEffect
  └─ 立ち上げたリソースは return で片付ける(fetch abort・listener 解除・clearInterval)

計算コストが高い or オブジェクト参照を安定させたい  → useMemo

子コンポーネントに渡す関数の参照を安定させたい  → useCallback + React.memo

深い props リレーを避けたい  → useContext
  └─ 変化頻度が違う値は別 context に分ける

useMemouseCallback は「まず動くものを作って、プロファイラで重いと確認してから入れる」が現実的な順序。最初から全部入れると、依存配列の管理コストが増えてバグの温床になる。

だれでもLP — その思いつき、3分でページになる。