はじめに

こんにちは!プログラミング初学者の私が、Reactプロジェクトに参加して「フォーム変更検知」という課題に取り組みました。この記事では、実際のプロジェクトで学んだReact Hooksの基礎を、初学者目線でわかりやすく解説します。

バックエンドの経験も数ヶ月程度なので、同じように学び始めたばかりの方にも理解しやすい内容を心がけています。

この記事で学べること

  • React Hooksの基礎(useState, useEffect, useRef, useMemo, useCallback, Jotai, カスタムフック、useRouter)
  • それぞれのHooksをいつ、どのように使うのか
  • 実際のコード例で理解する使い方

対象読者

  • Reactを始めたばかりの方
  • プログラミング初学者で、Reactを学びたい方
  • 「フォームの変更を検知して保存ボタンを活性化する」といった実装に悩んでいる方

1. なぜこの記事を書くのか

Reactプロジェクトに参加して、最初に困ったのが「フォームの変更を検知して保存ボタンを活性化する」という実装でした。

一見簡単そうに見えますが、いざ実装してみると

  • どのHooksを使えばいいの?
  • レンダリングって何?
  • 状態管理ってどうやるの?

といった疑問が次々と出てきました。

この記事では、同じように悩んでいる初学者の方に向けて、私が学んだReact Hooksの基礎を順番に解説していきます。

2. React Hooksとは?

Hooksの基本概念

React Hooksは、関数コンポーネントで状態管理や副作用を扱うための仕組みです。

簡単に言うと

  • useState: 「値を覚えておく」ための機能
  • useEffect: 「何かが変わったら処理を実行する」ための機能
  • useRef: 「再レンダリングされても保持される値」を扱う機能

といったように、それぞれが特定の役割を持っています。

なぜHooksが必要なのか?

Reactには、Hooksが登場する前に「Class Component」という書き方がありました。これはJavaScriptのクラス構文を使う方法で、初学者には少し難しい書き方でした。

Hooksは、Reactで関数を書くだけで状態管理ができる仕組みです。

Hooksのメリット:

  1. シンプルさ: 関数を書くだけでOK(クラスを書く必要がない)
  2. 再利用性: 同じロジックを複数のコンポーネントで使い回せる
  3. わかりやすさ: コードの見通しが良くなり、理解しやすい

これから学ぶ方へ:

現在(2025年)の主流はHooksなので、Class Componentを学ぶ必要はありません。Hooksから始めれば大丈夫です。

参考:React公式ドキュメント – Hooks では、Hooksを使った書き方が推奨されています。

3. プロジェクトで使用している主要なHooks

3.1 useState – 状態管理の基本

概念

useStateは、コンポーネント内で値を記憶するための機能です。値が変更されるとUIが自動的に再レンダリングされます。

基本構文

const [state, setState] = useState<型>(初期値);
  • state: 現在の状態値(読み取り専用)
  • setState: 状態を更新する関数
  • 初期値: コンポーネントの初回レンダリング時のみ使用

実装例:ユーザーの入力変更を検知

'use client';

export default function UserForm() {
  // 【1】状態の宣言(初期値: 空文字列、false)
  // hasUserChanges: ユーザーが何か変更したかのフラグ
  // itemName: 入力フィールドの値
  const [hasUserChanges, setHasUserChanges] = useState<boolean>(false);
  const [itemName, setItemName] = useState<string>('');

  // 【2】イベントハンドラ:入力フィールドが変更されたときに呼ばれる
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // 入力された値をitemNameに保存
    setItemName(e.target.value);  // → itemName = "商品A" (入力値が入る)
    // 変更フラグを立てる → ボタンが活性化される
    setHasUserChanges(true);  // → hasUserChanges = true になる
  };

  // 【3】画面の表示(レンダリング)
  return (
    <div>
      {/* 入力フィールド:ユーザーが文字を入力する */}
      <input
        type="text"
        value={itemName}           // 現在の値を表示
        onChange={handleChange}      // 入力されたら【2】が呼ばれる
      />
      {/* 保存ボタン:hasUserChangesがfalseの間は非活性 */}
      <button disabled={!hasUserChanges}>  {/* hasUserChanges=false → disabled=true (非活性), hasUserChanges=true → disabled=false (活性) */}
        保存
      </button>
    </div>
  );
}

このコードの流れ:

  1. 最初の表示
    • itemName = ''(空文字)
    • hasUserChanges = false
    • 画面:入力フィールドは空、ボタンは非活性(グレーアウト)
  2. ユーザーが入力(例:”商品A”と入力):
    • handleChangeが呼ばれる
    • e.target.value = "商品A"
  3. 状態が更新される
    • setItemName("商品A")itemName = "商品A"
    • setHasUserChanges(true)hasUserChanges = true
  4. 再レンダリング:Reactが自動的に画面を更新
  5. ボタンが活性化
    • hasUserChanges = trueなのでdisabled={!true}つまりdisabled={false}
    • ボタンが押せるようになる(青くなる)

重要なポイント:

  • ステート更新は非同期で行われる
  • 直接state = 新しい値のように変更してはいけない(イミュータブル)
  • ステートが更新されるとコンポーネントが再レンダリングされる

3.2 useEffect – 副作用の処理

概念

useEffectは、「特定の状態が変化したときに実行される処理」を定義します。データ取得やイベントリスナーの登録など、コンポーネントの表示以外の処理(副作用)を行うときに使います。

基本構文

useEffect(() => {
  // 実行したい処理

  return () => {
    // クリーンアップ処理(オプション)
  };
}, [依存配列]);

依存配列の挙動

依存配列 実行タイミング
[] マウント時のみ(初回レンダリング)
[state1, state2] マウント時 + state1またはstate2が変更されたとき
省略 毎回のレンダリング時(通常は使わない)

実装例1:初期化処理

useEffect(() => {
  // 【1】コンポーネントが画面に表示されたとき(マウント時)に1度だけ実行
  console.log('コンポーネントがマウントされました');
  // ここでAPIからデータを取得したり、初期設定をしたりする

  // 【2】クリーンアップ関数:コンポーネントが画面から消えるときに実行
  return () => {
    console.log('コンポーネントがアンマウントされました');
    // ここでタイマーを止めたり、イベントリスナーを解除したりする
  };
}, []);  // 空の依存配列 = マウント時のみ実行

このコードの流れ:

  1. コンポーネントが画面に表示される(マウント)
    • useEffectが実行される
    • コンソール:「コンポーネントがマウントされました」と表示される
    • 例:APIからデータ取得、初期設定など
  2. 画面に表示されている間:何も起きない(依存配列が[]なので再実行されない)
  3. コンポーネントが画面から消える(アンマウント)
    • クリーンアップ関数(returnの中)が実行される
    • コンソール:「コンポーネントがアンマウントされました」と表示される
    • 例:タイマーを停止、イベントリスナーを解除など

使用例:

  • データの取得
  • タイマーの設定
  • イベントリスナーの登録

実装例2:状態変化の監視

const [isInitialized, setIsInitialized] = useState<boolean>(false);

useEffect(() => {
  // 【1】条件チェック:必要な条件が揃っていなければ何もしない
  if (!edit || !incomeStatement || isInitialized) return;
  // → 例: edit=false なら何もしない, edit=true かつ isInitialized=false なら続行

  // 【2】1秒後に初期化完了フラグを立てる
  // なぜ待つ?自動計算などの処理が完了するのを待つため
  const timer = setTimeout(() => {
    setIsInitialized(true);  // → isInitialized = true になる
  }, 1000);  // 1秒待つ

  // 【3】クリーンアップ:コンポーネントが消えるときにタイマーを止める
  return () => clearTimeout(timer);  // タイマーをキャンセル
}, [edit, incomeStatement, isInitialized]);  // これらが変わったら再実行

このコードの流れ:

  1. edit、incomeStatement、isInitializedのいずれかが変わる
  2. 条件チェック:全て揃っていて、まだ初期化されていなければ…
  3. 1秒待つ:タイマーをセット
  4. 1秒後setIsInitialized(true)が実行される
  5. 再レンダリングisInitializedがtrueになったので画面が更新される

ポイント:
依存配列[edit, incomeStatement, isInitialized]に入っている値が変わると、このuseEffectが再実行されます。

実装例3:イベントリスナーの登録

export default function WindowWidth() {
  // 【1】画面幅を保存する状態
  const [width, setWidth] = useState(0);

  useEffect(() => {
    // 【2】画面幅を取得してstateに保存する関数
    const handleResize = () => setWidth(window.innerWidth);  // → width = 1920 (画面幅が入る)

    // 【3】最初に1回実行(初期値を設定)
    handleResize();  // → width = 1920

    // 【4】画面サイズが変わったら handleResize を呼ぶように登録
    window.addEventListener('resize', handleResize);

    // 【5】クリーンアップ:イベントリスナーを削除
    // これを忘れるとメモリリークが起きる!
    return () => window.removeEventListener('resize', handleResize);
  }, []);  // 空配列 = 最初の1回だけ実行

  // 【6】画面に表示
  return <p>画面幅: {width}px</p>;  // → 画面:「画面幅: 1920px」と表示される
}

このコードの流れ:

  1. コンポーネントが表示される
  2. useEffectが実行される
    • handleResize()で現在の画面幅を取得
    • イベントリスナーを登録(画面サイズが変わったら通知してもらう)
  3. ユーザーが画面サイズを変更
  4. handleResizeが呼ばれる:新しい画面幅をwidthに保存
  5. 再レンダリング:画面に新しい幅が表示される
  6. コンポーネントが消える:イベントリスナーを削除(メモリリーク防止)

ポイント:
イベントリスナーは必ずクリーンアップで削除する

重要なポイント

  • クリーンアップ関数は必ず実装する(メモリリーク防止)
  • 依存配列は正直に書く(ESLintが警告してくれる)
  • 無限ループに注意(useEffect内でステート更新 → 再レンダリング → useEffect実行…)

3.3 useRef – 値の参照保持

概念

useRefは、「再レンダリングされても保持される値」を扱います。useStateとの違いは、値を変更してもコンポーネントが再レンダリングされない点です。

基本構文

const refContainer = useRef<型>(初期値);

// 値の参照
refContainer.current

// 値の更新(再レンダリングは発生しない)
refContainer.current = 新しい値;

useStateとの違い

特徴 useState useRef
値の変更で再レンダリング ✅ する ❌ しない
用途 UI表示に関わる値 内部的な値、DOM参照
更新方法 setState関数 .current に代入

実装例1:初期値の保存

// 【1】初期値を保存するためのref(再レンダリングされても消えない)
const initialValues = useRef<{
  itemName: string;
  companyName: string;
} | null>(null);

useEffect(() => {
  // 【2】条件:編集モードで、データがあって、初期化済みで、まだ保存していなければ
  if (edit && incomeStatement && isInitialized && !initialValues.current) {
    // 【3】現在の値を初期値として保存(一度だけ)
    // useStateと違い、これを変更しても再レンダリングされない
    initialValues.current = {
      itemName,          // 現在の商品名
      companyName,        // 現在の会社名
    };
  }
}, [edit, incomeStatement, isInitialized]);

// 【4】後で初期値と比較して、変更があったかチェック
const hasChanges = itemName !== initialValues.current?.itemName;
// → 例: itemName="商品A改良版", initialValues.current.itemName="商品A"
//     → hasChanges = true (変更あり)

このコードの流れ:

  1. useRefで箱を作るinitialValuesという箱(最初は空 = null
  2. 条件が揃ったら:初期値を.currentに保存
    • 例:initialValues.current = { itemName: "商品A", companyName: "株式会社XYZ" }
  3. ユーザーが値を変更
    • 初期値:itemName = "商品A"
    • 変更後:itemName = "商品A改良版"(ユーザーが編集)
    • でも、initialValues.current.itemName"商品A"のまま
  4. 比較:現在の値と初期値を比べて、変更があったか判定
    • hasChanges = "商品A改良版" !== "商品A"true(変更あり)
    • この値を使って保存ボタンを活性化

useStateとの違い:

  • useState:値を変更すると再レンダリングされる
  • useRef:値を変更しても再レンダリングされない(内部的に値を保持したいときに使う)

実装例2:DOM要素への参照

// 【1】input要素を参照するためのref
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
  // 【2】コンポーネントが表示されたら、input要素にフォーカスを当てる
  inputRef.current?.focus();  // ?.はnullチェック(安全のため)
}, []);

// 【3】ref={inputRef}でinput要素とrefを紐付ける
return (
  <input ref={inputRef} type="text" />
);

このコードの流れ:

  1. inputRefを作成:最初はnull
  2. 画面にinput要素が表示されるref={inputRef}により、inputRef.currentに実際のDOM要素が入る
  3. useEffectが実行されるinputRef.current.focus()でフォーカスを当てる
  4. 結果:ページを開くと、自動的に入力フィールドにカーソルが入る

使用例:

  • 入力フィールドに自動フォーカス
  • スクロール位置の制御
  • 動画の再生/停止

重要なポイント

  • useRefの値変更は再レンダリングを発生させない
  • 初期値の保存や、前回の値との比較に最適
  • DOM操作にも使える(ただし、基本的には避ける)

3.4 useMemo – 計算結果のメモ化

概念

useMemoは、「計算コストの高い処理の結果をキャッシュする」仕組みです。同じ計算を何度も繰り返さないようにすることで、パフォーマンスを向上させます。

基本構文

const memoizedValue = useMemo(
  () => {
    // 計算処理
    return 計算結果;
  },
  [依存配列]
);

いつ使うべきか

  • 複雑な計算処理がある
  • 同じ入力に対して同じ出力を返す純粋な計算
  • 依存する値が変わらない限り、再計算する必要がない

実装例:複雑な計算のメモ化

// フィボナッチ数列を計算する関数(計算コストが高い)
const fibonacci = (n: number): number => {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
};

export default function Calculator() {
  // 【1】2つの状態を管理
  const [num, setNum] = useState(10);      // フィボナッチ計算に使う数
  const [count, setCount] = useState(0);   // 関係ない別のカウンター

  // 【2】useMemoで計算結果をキャッシュ
  // numが変わったときだけ再計算、countが変わっても再計算しない
  const result = useMemo(
    () => {
      console.log('フィボナッチ計算を実行');  // いつ計算されるか確認用
      return fibonacci(num);  // → num=10 なら result=55, num=11 なら result=89
    },
    [num]  // numが変わったときだけ再計算
  );

  return (
    <div>
      <p>フィボナッチ({num}) = {result}</p>
      {/* numボタン:クリックすると再計算される */}
      <button onClick={() => setNum(num + 1)}>numを増やす</button>
      {/* countボタン:クリックしても再計算されない! */}
      <button onClick={() => setCount(count + 1)}>countを増やす(再計算されない)</button>
      <p>count: {count}</p>
    </div>
  );
}

このコードの流れ:

  1. 最初の表示
    • fibonacci(10)が計算される → result = 55に保存
    • コンソール:「フィボナッチ計算を実行」と表示される
  2. 「numを増やす」ボタンをクリック
    • numが10 → 11になる
    • numが変わったのでuseMemoが再実行
    • fibonacci(11)が計算される → result = 89
    • コンソール:「フィボナッチ計算を実行」と表示される
  3. 「countを増やす」ボタンをクリック
    • countが0 → 1になる
    • 親コンポーネントが再レンダリング
    • でもnumは変わっていないのでuseMemo再実行されない
    • 前回のresult = 89がそのまま使われる(高速!)
    • コンソール:何も表示されない(計算していない証拠)

useMemoを使わない場合:
毎回のレンダリングでfibonacciが計算されてしまい、重い処理だと画面がカクつきます。

useMemoを使わない場合の問題

// ❌ 悪い例:毎回再計算される
export default function Calculator() {
  const [num, setNum] = useState(10);
  const [count, setCount] = useState(0);

  // countが変更されただけでも、fibonacci(num)が再計算される
  const result = fibonacci(num);

  return <p>結果: {result}</p>;
}

重要なポイント

  • 依存配列が変更されたときのみ再計算
  • 軽い計算では逆にオーバーヘッドになる場合もある
  • オブジェクトや配列の参照同一性を保つためにも使える

3.5 useCallback – 関数のメモ化

概念

useCallbackは、「関数自体をメモ化する」仕組みです。useMemoが値をキャッシュするのに対し、useCallbackは関数をキャッシュします。

基本構文

const memoizedCallback = useCallback(
  (引数) => {
    // 処理
  },
  [依存配列]
);

いつ使うべきか

  • 子コンポーネントにpropsとして関数を渡す場合
  • useEffectの依存配列に関数を含める場合
  • パフォーマンスが重要な場面

実装例:子コンポーネントへのprops渡し

import { useCallback } from 'react';

// 【1】子コンポーネント(React.memoでメモ化されている)
// propsが変わらなければ再レンダリングしない
const ChildComponent = React.memo(({ onClick }: { onClick: () => void }) => {
  console.log('子コンポーネントがレンダリングされました');
  return <button onClick={onClick}>カウントを増やす</button>;
});

export default function ParentComponent() {
  // 【2】2つの状態を管理
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // 【3】useCallbackで関数をメモ化
  // countが変わったときだけ新しい関数を作る
  const handleClick = useCallback(() => {
    setCount(count + 1);  // → count=0 なら count=1 になる
  }, [count]);  // countが変わったときだけ関数を再生成
  // → textが変わっても handleClick は同じ関数のまま(子コンポーネントは再レンダリングされない)

  return (
    <div>
      {/* テキスト入力 */}
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="何か入力してみてください"
      />
      <p>カウント: {count}</p>
      {/* 【4】子コンポーネントに関数を渡す */}
      {/* textが変わっても handleClick は同じなので子は再レンダリングされない */}
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

このコードの流れ:

  1. 最初の表示
    • count = 0, text = ''
    • handleClickが生成される(関数A)
    • 親コンポーネントと子コンポーネントがレンダリングされる
    • コンソール:「子コンポーネントがレンダリングされました」
  2. テキスト入力に文字を入力(例:”こんにちは”):
    • textが” → ‘こんにちは’に変わる
    • 親コンポーネントが再レンダリング
    • でもcountは変わっていないのでhandleClickは同じ関数A のまま
    • 子コンポーネントはReact.memoでメモ化されていて、propsのhandleClickは関数Aのまま
    • 子コンポーネントは再レンダリングされない
    • コンソール:何も表示されない(再レンダリングされていない証拠)
  3. 「カウントを増やす」ボタンをクリック
    • countが0 → 1に変わる
    • useCallbackの依存配列にcountがあるので新しい関数B が作られる
    • handleClickが関数A → 関数B に変わった
    • 子コンポーネントのpropsが変わったので再レンダリングされる
    • コンソール:「子コンポーネントがレンダリングされました」

useCallbackを使わない場合:
毎回新しい関数が作られるので、子コンポーネントが不要に再レンダリングされてしまいます。

useCallbackを使わない場合の問題

// ❌ 悪い例:毎回新しい関数が生成される
export default function ParentComponent() {
  // 再レンダリングのたびに新しい関数インスタンスが生成される
  const handleClick = () => {
    console.log('clicked');
  };

  // ChildComponentが React.memo でメモ化されていても、
  // handleClickが毎回新しい参照なので再レンダリングされてしまう
  return <ChildComponent onClick={handleClick} />;
}

// ✅ 良い例:useCallbackでメモ化
export default function ParentComponent() {
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);  // 依存なし = 常に同じ関数

  // ChildComponentの不要な再レンダリングを防げる
  return <ChildComponent onClick={handleClick} />;
}

useMemoとuseCallbackの違い

// useMemo: 値をメモ化
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

// useCallback: 関数をメモ化(以下は同じ意味)
const memoizedCallback = useCallback((arg) => doSomething(arg), [dependency]);
const memoizedCallback = useMemo(() => (arg) => doSomething(arg), [dependency]);

重要なポイント

  • 依存配列が変更されたときのみ関数を再生成
  • 子コンポーネントへのprops渡しで不要な再レンダリングを防ぐ
  • 全ての関数をuseCallbackでラップする必要はない(オーバーヘッド)

3.6 Jotai – グローバル状態管理

概念

Jotaiは、Reactのグローバル状態管理ライブラリです。複数のコンポーネント間で状態を共有したいときに使います。

基本構文

// Atomの定義(src/store/toastStore.ts)
import { atom } from 'jotai';

export const showToastAtom = atom<boolean>(false);
export const errorToastMessageAtom = atom<string>('');

// コンポーネントでの使用
import { useSetAtom, useAtomValue } from 'jotai';

function ErrorHandler() {
  // 書き込み専用
  const setShowToast = useSetAtom(showToastAtom);
  const setErrorMessage = useSetAtom(errorToastMessageAtom);

  const handleError = () => {
    setErrorMessage('エラーが発生しました');
    setShowToast(true);
  };
}

function ToastDisplay() {
  // 読み取り専用
  const showToast = useAtomValue(showToastAtom);
  const errorMessage = useAtomValue(errorToastMessageAtom);

  if (!showToast) return null;
  return <div>{errorMessage}</div>;
}

Jotaiの3つのHook

Hook 用途 返り値
useAtom 読み書き両方 [value, setValue]
useAtomValue 読み取りのみ value
useSetAtom 書き込みのみ setValue

実装例:エラーハンドリング

// 【1】トースト表示用のAtomを定義(グローバルな状態)
import { atom } from 'jotai';
import { useSetAtom } from 'jotai';

// トースト表示のON/OFF
export const showToastAtom = atom<boolean>(false);
// エラーメッセージの内容
export const errorMessageAtom = atom<string>('');

// 【2】エラーハンドリングのカスタムフック
export function useErrorHandler() {
  // Atomへの書き込み専用Hookを取得
  const setShowToast = useSetAtom(showToastAtom);
  const setErrorMessage = useSetAtom(errorMessageAtom);

  // 【3】エラーを処理する関数
  const handleError = (message: string) => {
    setErrorMessage(message);  // → errorMessageAtom = "データの取得に失敗しました"
    setShowToast(true);        // → showToastAtom = true (トースト表示)
  };

  return { handleError };  // この関数を他のコンポーネントで使えるように返す
}

// 【4】使用例:データ取得コンポーネント
export default function DataFetcher() {
  // エラーハンドラーを取得
  const { handleError } = useErrorHandler();

  // データ取得関数
  const fetchData = async () => {
    try {
      // 【5】APIからデータを取得
      const response = await fetch('/api/data');
      if (!response.ok) {
        // 【6】失敗したらエラーハンドラーを呼ぶ
        // → showToastAtom と errorMessageAtom が更新される
        handleError('データの取得に失敗しました');
      }
    } catch (error) {
      // 【7】予期しないエラーもハンドリング
      handleError('予期しないエラーが発生しました');
    }
  };

  return <button onClick={fetchData}>データ取得</button>;
}

このコードの流れ:

  1. Atomの定義showToastAtomerrorMessageAtomをグローバルに定義
  2. カスタムフック作成useErrorHandlerでエラー処理ロジックをまとめる
  3. コンポーネントで使用DataFetcherコンポーネントでuseErrorHandlerを呼び出す
  4. ボタンクリックfetchDataが実行される
  5. API呼び出し:データを取得しようとする
  6. エラー発生:APIが失敗するとhandleErrorが呼ばれる
  7. Atom更新setErrorMessagesetShowToastが実行される
  8. 別のコンポーネントで表示showToastAtomerrorMessageAtomを監視している別のコンポーネント(ToastDisplay等)が自動的に再レンダリングされ、エラーメッセージが表示される

ポイント:

  • Atomを使うことで、DataFetcherコンポーネントと表示用コンポーネントが直接つながっていなくても状態を共有できる
  • useErrorHandlerでロジックをまとめることで、他のコンポーネントでも同じエラー処理が使える

重要なポイント

  • コンポーネント間で状態を共有できる
  • 適切なHookを使い分けることでパフォーマンス向上
  • グローバルに管理すべき状態のみAtomにする(ローカルな状態はuseState

3.7 カスタムフック – ロジックの再利用

概念

カスタムフックは、「複数のコンポーネントで再利用したいロジック」を関数として切り出したものです。

命名規則

  • 必ずuseで始める(例: useIsMobile, useErrorHandler
  • Reactが「これはHookだ」と認識し、Hooksのルールを適用する

実装パターン:カスタムフックの構造

// カスタムフックの基本構造
export function useCustomHook(引数) {
  // 1. 他のHooksを使用できる
  const [state, setState] = useState(初期値);
  const ref = useRef(null);

  // 2. ロジックを実装
  useEffect(() => {
    // 副作用処理
  }, [依存配列]);

  // 3. 必要な値や関数を返す
  return { state, setState, ref };
}

実装例:データ取得のカスタムフック

import { useState, useEffect } from 'react';

// 【1】データ取得のカスタムフック(ジェネリック型T = 取得するデータの型)
export function useFetch<T>(url: string) {
  // 【2】3つの状態を管理
  const [data, setData] = useState<T | null>(null);      // 取得したデータ (初期値: null)
  const [loading, setLoading] = useState(true);          // 読み込み中か (初期値: true)
  const [error, setError] = useState<string | null>(null); // エラーメッセージ (初期値: null)

  useEffect(() => {
    // 【3】非同期でデータを取得する関数
    const fetchData = async () => {
      try {
        // 【4】APIからデータを取得
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('データの取得に失敗しました');
        }
        // 【5】JSONをパース
        const json = await response.json();  // → json = [{ id: 1, name: "田中太郎" }, ...]
        setData(json);  // → data = [{ id: 1, name: "田中太郎" }, ...]
      } catch (err) {
        // 【6】エラーをキャッチしてメッセージを保存
        setError(err instanceof Error ? err.message : '予期しないエラー');  // → error = "データの取得に失敗しました"
      } finally {
        // 【7】成功・失敗に関わらず、最後にローディングを終了
        setLoading(false);  // → loading = false
      }
    };

    fetchData();  // データ取得を開始
  }, [url]);  // urlが変わったら再実行

  // 【8】3つの値を返す
  return { data, loading, error };
}

// 【9】使用例:ユーザーリストコンポーネント
export default function UserList() {
  // カスタムフックを呼ぶだけ!データ取得のロジックは隠蔽されている
  const { data, loading, error } = useFetch<User[]>('/api/users');
  // → 最初: loading=true, data=null, error=null
  // → 成功後: loading=false, data=[{id:1, name:"田中太郎"}, ...], error=null
  // → 失敗後: loading=false, data=null, error="データの取得に失敗しました"

  // 【10】ローディング中の表示
  if (loading) return <p>読み込み中...</p>;  // loading=true のときここが表示される
  // 【11】エラー時の表示
  if (error) return <p>エラー: {error}</p>;  // error がある場合ここが表示される

  // 【12】データ取得成功時の表示
  return (
    <ul>
      {data?.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

このコードの流れ:

  1. コンポーネントがレンダリングUserListが画面に表示される
  2. useFetchが実行url = '/api/users'で呼び出される
  3. 初期状態
    • loading = true
    • data = null
    • error = null
  4. useEffectが実行
    • fetchData()が呼ばれる
    • fetch('/api/users')でAPIにリクエストを送る
  5. 待機中loading = trueなので画面には「読み込み中…」と表示される
  6. API成功の場合
    • レスポンス例:[{ id: 1, name: "田中太郎" }, { id: 2, name: "鈴木花子" }]
    • setData(json)でデータを保存 → data = [{ id: 1, name: "田中太郎" }, ...]
    • setLoading(false)loading = false
    • 再レンダリング → ユーザーリストが表示される
    • 画面:
      - 田中太郎
      - 鈴木花子
      
  7. API失敗の場合(例:サーバーエラー500):
    • setError("データの取得に失敗しました")error = "データの取得に失敗しました"
    • setLoading(false)loading = false
    • 再レンダリング → エラーメッセージが表示される
    • 画面:エラー: データの取得に失敗しました

カスタムフックを使う利点:

  • 同じデータ取得ロジックを複数のコンポーネントで再利用できる
  • コンポーネントのコードがシンプルになる(ロジックが隠蔽されている)
  • テストしやすい(useFetchだけを単独でテストできる)

カスタムフックを作るべきタイミング

  1. 同じロジックが複数のコンポーネントで使われている
  2. 複雑な状態管理ロジックをコンポーネントから分離したい
  3. テストしやすくしたい

重要なポイント

  • カスタムフック内で他のHooksを自由に使える
  • コンポーネントの可読性が向上する
  • テストが容易になる(ロジックを単独でテストできる)

3.8 useRouter – Next.js 15のルーティング

概念

useRouterは、Next.jsのルーティング機能を制御するHookです。ページ遷移やURLの操作ができます。

基本構文

'use client';

import { useRouter } from 'next/navigation';

export default function MyComponent() {
  const router = useRouter();

  // ページ遷移
  router.push('/users');

  // 戻る
  router.back();

  // リフレッシュ
  router.refresh();
}

実装例:フォーム送信後の遷移

'use client';  // Client Componentとして宣言(useRouterを使うため)

import { useRouter } from 'next/navigation';

export default function CreateUser() {
  // 【1】useRouterでルーター機能を取得
  const router = useRouter();

  // 【2】フォーム送信時の処理
  const handleSubmit = async (data: FormData) => {
    try {
      // 【3】APIにデータを送信(新しいユーザーを作成)
      await fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(data),  // → { name: "田中太郎", email: "tanaka@example.com" }
      });

      // 【4】成功したらユーザー一覧画面へ遷移
      // router.push() = 新しいページへ移動(ブラウザの履歴に追加される)
      router.push('/users');  // → '/users' ページに移動
    } catch (error) {
      // 【5】エラーが発生したらコンソールに出力
      console.error(error);  // → コンソールにエラーログが出力される
      // 実際のアプリでは、エラートーストを表示するなどの処理を入れる
    }
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

このコードの流れ:

  1. フォームが表示される:ユーザーが入力できる状態
  2. ユーザーがフォームを入力して送信handleSubmitが呼ばれる
  3. APIにデータ送信fetchでPOSTリクエスト
  4. API処理が成功:新しいユーザーがデータベースに保存される
  5. ページ遷移router.push('/users')で一覧画面へ移動
  6. 一覧画面が表示される:作成したユーザーが一覧に含まれている

router の主なメソッド:

メソッド 説明
router.push('/path') 指定したパスに遷移 router.push('/users')
router.back() 前のページに戻る ブラウザの「戻る」ボタンと同じ
router.refresh() 現在のページを再読み込み サーバーから最新データを取得

重要なポイント:

  • useRouterClient Componentでのみ使用できる('use client';が必要)
  • Server Componentでページ遷移したい場合は、redirect()関数を使う

4. 今回学んだこと

4.1 どのHookをいつ使うか?

Hook 使用場面
useState コンポーネントのUI状態 入力値、モーダル表示状態
useEffect 副作用処理、初期化 API呼び出し、イベントリスナー登録
useRef 再レンダリング不要な値 初期値保存、DOM参照
useMemo 計算結果のキャッシュ 複雑な計算、配列のフィルタリング
useCallback 関数のメモ化 子コンポーネントへのprops
useAtom グローバル状態(読み書き) 認証情報、テーマ設定
useAtomValue グローバル状態(読み取り) トースト表示判定
useSetAtom グローバル状態(書き込み) エラーメッセージ設定
カスタムフック 再利用可能なロジック フォームバリデーション、API呼び出し

4.2 実装で気をつけたいポイント

1. useEffectの依存配列は必ず指定する

// ❌ 悪い例:依存配列を省略
useEffect(() => {
  console.log(someValue);
}, []);  // someValueが変更されても実行されない

// ✅ 良い例:必要な依存を全て含める
useEffect(() => {
  console.log(someValue);
}, [someValue]);

2. クリーンアップ関数を忘れない

useEffect(() => {
  const handleResize = () => console.log('resized');
  window.addEventListener('resize', handleResize);

  // クリーンアップでイベントリスナーを削除
  return () => window.removeEventListener('resize', handleResize);
}, []);

3. useRefとuseStateの違いを理解する

  • useState: 値が変更されると再レンダリングが発生する
  • useRef: 値が変更されても再レンダリングは発生しない

5. まとめ

学んだこと

  1. useState: UIに関わる状態管理の基本
  2. useEffect: 副作用処理とライフサイクル管理
  3. useRef: 再レンダリング不要な値の保持
  4. useMemo: 計算結果のメモ化でパフォーマンス向上
  5. useCallback: 関数のメモ化で不要な再レンダリング防止
  6. Jotai: グローバル状態管理(useAtom, useAtomValue, useSetAtom)
  7. カスタムフック: ロジックの再利用とテスタビリティ向上
  8. useRouter: Next.jsのルーティング制御

Reactの学習で最も重要なのは、「状態に応じてUIが変わる」という考え方です。

例えば:

  • ログインしている → ユーザー名を表示
  • ログインしていない → ログインボタンを表示

このように、「状態」によって「表示するもの」が変わる、というのがReactの基本です。

Hooksは、この「状態」を管理するための道具だと考えると理解しやすくなります。

参考資料


この記事が、React初心者の方やバックエンドからフロントエンドに挑戦する方の助けになれば幸いです!

質問やフィードバックがあれば、ぜひコメントでお知らせください 📝