React Hooks入門:コード例付きチュートリアル

Hooksは関数コンポーネントで状態や副作用、コンテキスト、メモ化、外部ストア、DOM連携を扱うためのAPI群です。ここではReact18以降を前提に、Hooksの実用的な使い方と設計の勘所、落とし穴、パフォーマンス上の考え方を JavaScriptのコードサンプルと共に解説します。

useState — 最小の状態管理と関数型更新

関数型更新を使うと、更新が重なっても常に最新の状態から次の値を計算できるので、連続更新や非同期処理でも値のズレを防げます。さらに、useEffectなどでタイマーやイベント登録といった副作用を使ったときは、コンポーネントが外れる時にそれらを解除する後片付け(クリーンアップ)を必ず書きましょう。

jsx
import { useEffect, useState } from "react";

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

  const inc = () => setCount((c) => c + 1);

  useEffect(() => {
    const id = setInterval(() => setCount((c) => c + 1), 1000);
    return () => clearInterval(id);
  }, []);

  return <button onClick={inc}>Count: {count}</button>;
}

useEffect — 副作用とクリーンアップ、依存配列の原則

データ取得のような処理は、不要になったときに途中でキャンセルできるようにしておきましょう。さらにuseEffectの依存配列には、その中で使っている値や関数を入れて、入れ忘れによるバグや無駄な再実行を防ぎましょう。

jsx
import { useEffect, useState } from "react";

export function UsersList() {
  const [users, setUsers] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const ac = new AbortController();

    (async () => {
      try {
        setError(null);
        const res = await fetch("/api/users", { signal: ac.signal });
        if (!res.ok) throw new Error("Network error");
        const data = await res.json();
        setUsers(data);
      } catch (e) {
        if (e.name !== "AbortError") setError(e);
      }
    })();

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

  if (error) return <p role="alert">{error.message}</p>;
  if (!users) return <p>Loading...</p>;

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

useRef — ミュータブルな箱とDOM参照

再レンダリングなしで値を保持したいときや、DOM要素へ直接アクセスしたいときに使います。前回値を保持する小さなカスタムHookもよく使います。

jsx
import { useEffect, useRef, useState } from "react";

export function SearchBox() {
  const inputRef = useRef(null);
  const [q, setQ] = useState("");

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

  return (
    <label>
      Query:
      <input ref={inputRef} value={q} onChange={(e) => setQ(e.target.value)} />
    </label>
  );
}
jsx
import { useEffect, useRef } from "react";

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

useMemo / useCallback — 無駄な計算や再生成の抑制

高コスト計算や巨大配列の絞り込み、メモ化したコールバックをpropsで渡すケースに絞って使うと効果的です。

jsx
import { memo, useCallback, useMemo, useState } from "react";

const Row = memo(function Row({ item, onSelect }) {
  return <li onClick={() => onSelect(item.id)}>{item.label}</li>;
});

export function FilteredList({ items }) {
  const [q, setQ] = useState("");

  const filtered = useMemo(() => {
    const lower = q.toLowerCase();
    return items.filter((i) => i.label.toLowerCase().includes(lower));
  }, [items, q]);

  const handleSelect = useCallback((id) => {
    console.log("selected", id);
  }, []);

  return (
    <>
      <input
        placeholder="search"
        value={q}
        onChange={(e) => setQ(e.target.value)}
      />
      <ul>
        {filtered.map((i) => (
          <Row key={i.id} item={i} onSelect={handleSelect} />
        ))}
      </ul>
    </>
  );
}

useReducer — 複雑な状態やイベント駆動の更新に

状態遷移をreducerに集約すると、意図が明確になりテストしやすくなります。

jsx
import { useReducer, useState } from "react";

function uid() {
  return Math.random().toString(36).slice(2, 10);
}

function reducer(state, action) {
  switch (action.type) {
    case "add":
      return [...state, { id: uid(), text: action.text, done: false }];
    case "toggle":
      return state.map((t) =>
        t.id === action.id ? { ...t, done: !t.done } : t
      );
    case "remove":
      return state.filter((t) => t.id !== action.id);
    default:
      return state;
  }
}

export function TodoApp() {
  const [todos, dispatch] = useReducer(reducer, []);
  const [text, setText] = useState("");

  return (
    <>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (text.trim()) dispatch({ type: "add", text });
          setText("");
        }}
      >
        <input
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="Add todo"
        />
        <button>Add</button>
      </form>
      <ul>
        {todos.map((t) => (
          <li key={t.id}>
            <label>
              <input
                type="checkbox"
                checked={t.done}
                onChange={() => dispatch({ type: "toggle", id: t.id })}
              />
              {t.text}
            </label>
            <button onClick={() => dispatch({ type: "remove", id: t.id })}>
              x
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

useContext — グローバル依存の明示化

読み取り用のカスタムHookを用意すると扱いやすくなります。依存性を明示でき、疎結合な設計につながります。

jsx
import { createContext, useContext, useMemo, useState } from "react";

const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  const value = useMemo(() => ({ theme, setTheme }), [theme]);
  return (
    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
  );
}

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be inside ThemeProvider");
  return ctx;
}

並行レンダリング対応 — useTransitionと useDeferredValue

入力のレスポンスを優先しつつ重い描画を遅らせたいときはuseTransitionを使いましょう。値だけ遅延させたい場合は useDeferredValueがシンプルです。

jsx
import { useState, useTransition } from "react";

export function SearchWithTransition() {
  const [input, setInput] = useState("");
  const [query, setQuery] = useState("");
  const [isPending, startTransition] = useTransition();

  const onChange = (v) => {
    setInput(v);
    startTransition(() => setQuery(v));
  };

  return (
    <>
      <input
        value={input}
        onChange={(e) => onChange(e.target.value)}
        placeholder="search"
      />
      {isPending && <p>Updating...</p>}
      <LargeList filter={query} />
    </>
  );
}

function LargeList({ filter }) {
  return <div>Filtering by: {filter}</div>;
}
jsx
import { useDeferredValue, useMemo, useState } from "react";

export function SearchWithDeferred({ items }) {
  const [q, setQ] = useState("");
  const deferredQ = useDeferredValue(q);

  const results = useMemo(() => {
    return items.filter((s) => s.includes(deferredQ));
  }, [items, deferredQ]);

  return (
    <>
      <input value={q} onChange={(e) => setQ(e.target.value)} />
      <div>{results.length} items</div>
    </>
  );
}

useId — SSR/CSRで安定した関連付け

フォームのラベルやARIA属性の関連付けに、サーバーとクライアントで一致する安定IDを生成します。

jsx
import { useId } from "react";

export function LabeledInput() {
  const id = useId();
  return (
    <div>
      <label htmlFor={id}>Name</label>
      <input id={id} />
    </div>
  );
}

useSyncExternalStore — 外部ストアと一致性のとれた購読

ブラウザAPIの状態を購読する例です。SSR時のスナップショットを渡せます。

jsx
import { useSyncExternalStore } from "react";

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

function getSnapshot() {
  return navigator.onLine;
}

function getServerSnapshot() {
  return true;
}

export function OnlineBadge() {
  const online = useSyncExternalStore(
    subscribe,
    getSnapshot,
    getServerSnapshot
  );
  return <span>{online ? "Online" : "Offline"}</span>;
}

useLayoutEffect — DOM計測や同期レイアウトが必要なとき

描画直後に同期で実行されます。測定やフリッカー防止に使います。可能なら通常はuseEffectを優先します。

jsx
import { useLayoutEffect, useRef, useState } from "react";

export function MeasureBox() {
  const ref = useRef(null);
  const [rect, setRect] = useState(null);

  useLayoutEffect(() => {
    if (ref.current) setRect(ref.current.getBoundingClientRect());
  }, []);

  return (
    <div>
      <div
        ref={ref}
        style={{ width: 200, height: 120, background: "#eef" }}
      />
      <pre>{rect ? JSON.stringify(rect.toJSON()) : "measuring..."}</pre>
    </div>
  );
}

forwardRef + useImperativeHandle — 子から命令的API を公開

フォーカスやスクロールなど命令的操作を安全にエクスポートします。

jsx
import { forwardRef, useImperativeHandle, useRef, useState } from "react";

export const TextInput = forwardRef(function TextInput({ initial = "" }, ref) {
  const inputRef = useRef(null);
  const [value, setValue] = useState(initial);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    clear: () => setValue(""),
  }));

  return (
    <input
      ref={inputRef}
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
});
jsx
import { useRef } from "react";
import { TextInput } from "./TextInput";

export function ImperativeExample() {
  const ref = useRef(null);
  return (
    <div>
      <TextInput ref={ref} />
      <button onClick={() => ref.current?.focus()}>Focus</button>
      <button onClick={() => ref.current?.clear()}>Clear</button>
    </div>
  );
}

よくある落とし穴

  • 依存配列の漏れは最も多い不具合源です。オブジェクトや関数を毎回インラインで生成すると依存が安定せず、再実行が暴発します。必要に応じてuseMemoとuseCallbackで参照を安定化します。
  • 単なる描画用の派生値にuseEffectは不要です。レンダリング中の計算で十分な場合、state化しないほうがループや整合性の問題を避けられます。
  • useRefに入れた値を変更しても再描画はされません。UIに反映したいならstateを使います。逆に、再描画を発生させたくない一時的な参照はrefに置きます。

まとめ

Hooksは状態管理(useState/useReducer)、副作用(useEffect)、参照(useRef)、メモ化(useMemo/useCallback)、コンテキスト(useContext)、並行 UI(useTransition/useDeferredValue)、外部ストア(useSyncExternalStore)、DOM同期(useLayoutEffect)を押さえると設計しやすくなります。

69

おすすめ記事