React Hooksのルールとは?守らないとエラーになる2つの掟

React Hooksの2つの重要なルールとその理由を詳しく解説。違反例から正しい使い方まで、初心者が陥りやすい落とし穴を避ける方法を実践的に紹介

Learning Next 運営
44 分で読めます

みなさん、React Hooksを使っているとき「Invalid hook call」というエラーに遭遇したことはありませんか?

「Hooks can only be called inside the body of a function component」 「なんでこのコードでエラーが出るの?」 「どこを直せばいいのか分からない」

こんな経験をしたことがある方も多いはずです。

実は、React Hooksには守らないとエラーになる2つの重要なルールがあります。 この記事では、React Hooksの2つの重要なルールについて詳しく解説します。

なぜこれらのルールが存在するのか、違反するとどんなエラーが起こるのか、そして正しい使い方まで具体的な例とともにお伝えしますよ。 ぜひ最後まで読んで、エラーに悩まされない安全なReact開発をマスターしてくださいね!

React Hooksの2つの大切なルールを知ろう

React Hooksには、正しく動作させるために必ず守らなければならない2つのルールがあります。 まずは、この2つのルールをしっかりと理解しましょう。

絶対に守るべき2つのルール

React Hooksの基本ルールを見てみましょう。

ルール1: Hooksはトップレベルでのみ呼び出す

  • ループの中で使っちゃダメ
  • if文の中で使っちゃダメ
  • 関数の中の関数で使っちゃダメ

ルール2: React関数コンポーネントまたはカスタムHookからのみ呼び出す

  • 普通のJavaScript関数では使っちゃダメ
  • クラスコンポーネントでは使っちゃダメ
  • イベントハンドラーの中では使っちゃダメ
// React Hooksの2つの基本ルール
const hooksRules = {
  ルール1: {
    内容: "Hooksはトップレベルでのみ呼び出す",
    理由: "Hooksの呼び出し順序を一定に保つため"
  },
  
  ルール2: {
    内容: "React関数コンポーネントまたはカスタムHookからのみ呼び出す",
    理由: "Reactの内部管理システムとの整合性を保つため"
  }
};

なんだか難しそうに見えますが、大丈夫です! 一つずつ丁寧に解説していきますよ。

なぜこんなルールがあるの?

Reactの内部の仕組みを理解すると、なぜこのルールが必要なのかが分かります。

// Hooksの内部動作の概念
const whyRulesExist = {
  Reactの内部管理: {
    説明: "ReactはHooksを配列として管理している",
    例: "useState, useEffect, useContext の呼び出し順序が重要"
  },
  
  順序が大切: {
    説明: "毎回同じ順序でHooksが呼ばれることを前提としている",
    例: "初回: [state1, state2, effect1] → 再レンダリング: [state1, state2, effect1]"
  },
  
  状態の管理: {
    説明: "Hooksの呼び出し順序で状態を識別している",
    危険: "順序が変わると、間違った状態が返される可能性"
  }
};

簡単に言うと、Reactは「1番目のHooksは○○の状態、2番目のHooksは△△の状態」という風に覚えているんです。 だから、順序が変わってしまうと混乱してしまうんですね。

ルール1: トップレベルでの呼び出しを守ろう

最初のルールについて詳しく見てみましょう。 間違いやすいパターンから正しい書き方まで、実際のコードで確認していきますよ。

よくある間違い:条件分岐の中でHooksを使う

まずは、やってしまいがちな間違いから見てみましょう。

// ❌ 間違った例:条件分岐の中でuseStateを使用
function UserProfile({ user }) {
  // これはエラーになる!
  if (user) {
    const [name, setName] = useState(user.name); // ルール違反
  }
  
  return <div>User Profile</div>;
}

// ❌ 間違った例:早期リターンの後でHooksを使用
function UserDashboard({ isLoggedIn }) {
  if (!isLoggedIn) {
    return <div>Please log in</div>; // ここで早期リターン
  }
  
  // この位置でのHooks使用はエラーになる
  const [data, setData] = useState(null); // ルール違反
  
  return <div>Dashboard</div>;
}

これらのコードは、一見問題なさそうに見えますよね。 でも、Hooksのルールに違反しているためエラーになってしまいます。

正しい書き方:Hooksを最初に宣言する

正しい書き方を見てみましょう。

// ✅ 正しい例:Hooksをトップレベルで呼び出し
function UserProfile({ user }) {
  // Hooksは常にトップレベルで呼び出す
  const [name, setName] = useState(user?.name || '');
  const [email, setEmail] = useState(user?.email || '');
  const [isEditing, setIsEditing] = useState(false);
  
  // 条件分岐はHooksの後に行う
  if (!user) {
    return <div>ユーザー情報がありません</div>;
  }
  
  return (
    <div>
      {isEditing ? (
        <form>
          <input 
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
          <input 
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
          <button onClick={() => setIsEditing(false)}>保存</button>
        </form>
      ) : (
        <div>
          <p>名前: {name}</p>
          <p>メール: {email}</p>
          <button onClick={() => setIsEditing(true)}>編集</button>
        </div>
      )}
    </div>
  );
}

この書き方なら、毎回同じ順序でHooksが呼ばれるので安全です。 条件によって表示内容を変えたい場合は、Hooksの後で条件分岐しましょう。

ダッシュボードの正しい実装

// ✅ 正しい例:条件付きロジックを適切に処理
function UserDashboard({ isLoggedIn }) {
  // Hooksを最初に全て宣言
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  // useEffectも条件に関係なく宣言
  useEffect(() => {
    if (isLoggedIn) {
      setLoading(true);
      fetchUserData()
        .then(setData)
        .catch(setError)
        .finally(() => setLoading(false));
    }
  }, [isLoggedIn]);
  
  // 条件分岐による表示制御
  if (!isLoggedIn) {
    return <div>ログインしてください</div>;
  }
  
  if (loading) {
    return <div>読み込み中...</div>;
  }
  
  if (error) {
    return <div>エラー: {error.message}</div>;
  }
  
  return (
    <div>
      <h1>ダッシュボード</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

useEffectの中で条件分岐することで、Hooksのルールを守りながら適切に処理できます。

ループの中でHooksを使うのもNG

ループの中でHooksを使うのも違反です。 正しい方法を見てみましょう。

// ❌ 間違った例:ループ内でHooksを使用
function TaskList({ tasks }) {
  const taskStates = [];
  
  // これはエラーになる!
  tasks.forEach((task, index) => {
    const [completed, setCompleted] = useState(task.completed); // ルール違反
    taskStates.push({ completed, setCompleted });
  });
  
  return <div>Task List</div>;
}

この書き方では、タスクの数だけHooksが動的に作られてしまいます。 Reactは毎回同じ数のHooksが呼ばれることを期待しているので、エラーになってしまいますね。

正しい解決方法1: 配列状態を使用

// ✅ 正しい例:配列状態として管理
function TaskList({ tasks }) {
  // 配列状態として管理
  const [taskStates, setTaskStates] = useState(
    tasks.map(task => ({ ...task, completed: task.completed }))
  );
  
  const toggleTask = (index) => {
    setTaskStates(prev => 
      prev.map((task, i) => 
        i === index ? { ...task, completed: !task.completed } : task
      )
    );
  };
  
  return (
    <div>
      {taskStates.map((task, index) => (
        <div key={task.id}>
          <input 
            type="checkbox"
            checked={task.completed}
            onChange={() => toggleTask(index)}
          />
          <span>{task.title}</span>
        </div>
      ))}
    </div>
  );
}

配列として状態を管理することで、Hooksのルールを守りながら複数のタスクを扱えます。

正しい解決方法2: 個別コンポーネントに分離

// ✅ または個別コンポーネントに分離
function TaskItem({ task, onToggle }) {
  const [isHovered, setIsHovered] = useState(false);
  
  return (
    <div 
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      style={{ backgroundColor: isHovered ? '#f0f0f0' : 'white' }}
    >
      <input 
        type="checkbox"
        checked={task.completed}
        onChange={onToggle}
      />
      <span>{task.title}</span>
    </div>
  );
}

function TaskList({ tasks }) {
  const [taskList, setTaskList] = useState(tasks);
  
  const toggleTask = (taskId) => {
    setTaskList(prev =>
      prev.map(task =>
        task.id === taskId ? { ...task, completed: !task.completed } : task
      )
    );
  };
  
  return (
    <div>
      {taskList.map(task => (
        <TaskItem 
          key={task.id}
          task={task}
          onToggle={() => toggleTask(task.id)}
        />
      ))}
    </div>
  );
}

個別のコンポーネントに分離することで、各タスクアイテムで独自の状態を持てます。 コンポーネントの責任も明確になって、保守しやすくなりますね。

ルール2: React関数からのみ呼び出そう

2つ目のルールについて詳しく見てみましょう。 Hooksは特定の場所でしか使えないんです。

よくある間違い:普通の関数でHooksを使う

まずは、間違った使い方を見てみましょう。

// ❌ 間違った例:通常のJavaScript関数でHooksを使用
function fetchUserData(userId) {
  // これはエラーになる!
  const [data, setData] = useState(null); // ルール違反
  
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(response => response.json())
      .then(setData);
  }, [userId]);
  
  return data;
}

// ❌ 間違った例:イベントハンドラ内でHooksを使用
function UserProfile() {
  const handleClick = () => {
    // これはエラーになる!
    const [loading, setLoading] = useState(false); // ルール違反
    setLoading(true);
  };
  
  return <button onClick={handleClick}>クリック</button>;
}

普通のJavaScript関数やイベントハンドラーの中では、Hooksを使えません。 Reactの管理下にない場所だからです。

正しい書き方:React関数コンポーネントで使う

正しい使い方を見てみましょう。

// ✅ 正しい例:React関数コンポーネントでHooksを使用
function UserProfile({ userId }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error('Failed to fetch user data');
        }
        
        const userData = await response.json();
        setData(userData);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
  }, [userId]);
  
  const handleRefresh = () => {
    // イベントハンドラ内では状態更新のみ
    setData(null);
    setError(null);
    // useEffectが再実行されてデータを再取得
  };
  
  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  if (!data) return <div>データがありません</div>;
  
  return (
    <div>
      <h2>{data.name}</h2>
      <p>{data.email}</p>
      <button onClick={handleRefresh}>更新</button>
    </div>
  );
}

React関数コンポーネントの中でHooksを使うのが正しい方法です。 イベントハンドラーの中では、状態更新関数だけを呼び出しましょう。

カスタムHookでロジックを分離しよう

関連するロジックをカスタムHookに分離することもできます。

// ✅ 正しい例:カスタムHookでロジックを分離
function useUserData(userId) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      
      const response = await fetch(`/api/users/${userId}`);
      const userData = await response.json();
      setData(userData);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [userId]);
  
  useEffect(() => {
    fetchData();
  }, [fetchData]);
  
  return { data, loading, error, refetch: fetchData };
}

// カスタムHookを使用するコンポーネント
function UserProfile({ userId }) {
  const { data, loading, error, refetch } = useUserData(userId);
  
  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  if (!data) return <div>データがありません</div>;
  
  return (
    <div>
      <h2>{data.name}</h2>
      <p>{data.email}</p>
      <button onClick={refetch}>更新</button>
    </div>
  );
}

カスタムHookはuseで始まる名前にする必要があります。 これもReactのルールの一つですね。

カスタムHookの作り方をマスターしよう

カスタムHookの正しい作成方法を詳しく見てみましょう。

// ✅ カスタムHook:useで始まる関数名
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  
  const increment = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);
  
  const decrement = useCallback(() => {
    setCount(prev => prev - 1);
  }, []);
  
  const reset = useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);
  
  return { count, increment, decrement, reset };
}

// ✅ カスタムHook:API呼び出しの抽象化
function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const execute = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err);
    } finally {
      setLoading(false);
    }
  }, [url]);
  
  useEffect(() => {
    execute();
  }, [execute]);
  
  return { data, loading, error, refetch: execute };
}

// カスタムHookの使用例
function UserList() {
  const { data: users, loading, error, refetch } = useApi('/api/users');
  const { count, increment, reset } = useCounter(0);
  
  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error.message}</div>;
  
  return (
    <div>
      <div>
        <p>カウント: {count}</p>
        <button onClick={increment}>+1</button>
        <button onClick={reset}>リセット</button>
      </div>
      
      <div>
        <h2>ユーザー一覧</h2>
        <button onClick={refetch}>再取得</button>
        <ul>
          {users?.map(user => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

カスタムHookを使うことで、ロジックを再利用しやすくなります。 複数のコンポーネントで同じような処理が必要な場合に便利ですよ。

よくあるエラーと解決方法

実際によく発生するエラーと対処法をご紹介します。 エラーメッセージを見れば、何が問題なのかが分かりますよ。

エラー1: "Invalid hook call"

このエラーの原因と解決方法を見てみましょう。

// エラーの原因例
const errorCauses = {
  原因1: "React とReact-DOMのバージョン不一致",
  原因2: "同じアプリで複数のReactコピーが存在",
  原因3: "Hooksのルール違反",
  原因4: "クラスコンポーネント内でのHooks使用"
};

// ❌ エラーが発生するパターン
function App() {
  return (
    <div>
      {/* 条件付きでコンポーネントを表示 */}
      {Math.random() > 0.5 && <ProblematicComponent />}
    </div>
  );
}

function ProblematicComponent() {
  // このHooksは条件付きで呼ばれるため、エラーの原因となる可能性
  const [value, setValue] = useState(0);
  
  return <div>{value}</div>;
}

この例では、コンポーネント自体が条件付きで表示されるため、Hooksの呼び出し順序が不安定になってしまいます。

正しい解決方法

// ✅ 正しい解決方法
function App() {
  const [showComponent, setShowComponent] = useState(Math.random() > 0.5);
  
  return (
    <div>
      <button onClick={() => setShowComponent(!showComponent)}>
        Toggle Component
      </button>
      {showComponent && <SafeComponent />}
    </div>
  );
}

function SafeComponent() {
  const [value, setValue] = useState(0);
  
  return (
    <div>
      <p>値: {value}</p>
      <button onClick={() => setValue(v => v + 1)}>増加</button>
    </div>
  );
}

状態でコンポーネントの表示を制御することで、安全に管理できます。

エラー2: "Hooks can only be called inside the body of a function component"

このエラーの対処法を見てみましょう。

// ❌ エラーが発生するコード
const utils = {
  createUser: (userData) => {
    // ユーティリティ関数内でHooksを使用(エラー)
    const [loading, setLoading] = useState(false);
    
    setLoading(true);
    return fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(userData)
    });
  }
};

ユーティリティ関数の中ではHooksが使えません。

解決方法1: カスタムHookに移動

// ✅ 正しい解決方法1: カスタムHookに移動
function useCreateUser() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const createUser = useCallback(async (userData) => {
    try {
      setLoading(true);
      setError(null);
      
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData)
      });
      
      if (!response.ok) {
        throw new Error('Failed to create user');
      }
      
      return await response.json();
    } catch (err) {
      setError(err.message);
      throw err;
    } finally {
      setLoading(false);
    }
  }, []);
  
  return { createUser, loading, error };
}

解決方法2: 通常の非同期関数として実装

// ✅ 正しい解決方法2: 通常の非同期関数として実装
const userAPI = {
  createUser: async (userData) => {
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(userData)
    });
    
    if (!response.ok) {
      throw new Error('Failed to create user');
    }
    
    return await response.json();
  }
};

// コンポーネント内でのAPI使用
function UserForm() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [formData, setFormData] = useState({ name: '', email: '' });
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      setLoading(true);
      setError(null);
      
      await userAPI.createUser(formData);
      setFormData({ name: '', email: '' });
      alert('ユーザーが作成されました');
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        value={formData.name}
        onChange={(e) => setFormData(prev => ({...prev, name: e.target.value}))}
        placeholder="名前"
      />
      <input 
        type="email"
        value={formData.email}
        onChange={(e) => setFormData(prev => ({...prev, email: e.target.value}))}
        placeholder="メールアドレス"
      />
      <button type="submit" disabled={loading}>
        {loading ? '作成中...' : 'ユーザー作成'}
      </button>
      {error && <p style={{ color: 'red' }}>エラー: {error}</p>}
    </form>
  );
}

状態管理が必要な場合はカスタムHook、単純なAPI呼び出しなら通常の関数として実装しましょう。

エラー3: 条件付きuseEffectの問題

useEffectも条件付きで使うとエラーになります。

// ❌ 条件付きでuseEffectを使用(エラー)
function DataFetcher({ shouldFetch, url }) {
  const [data, setData] = useState(null);
  
  // これはルール違反
  if (shouldFetch) {
    useEffect(() => {
      fetch(url)
        .then(response => response.json())
        .then(setData);
    }, [url]);
  }
  
  return <div>{data ? JSON.stringify(data) : 'No data'}</div>;
}

正しい解決方法

// ✅ 正しい解決方法
function DataFetcher({ shouldFetch, url }) {
  const [data, setData] = useState(null);
  
  // useEffectは常に呼び出し、内部で条件分岐
  useEffect(() => {
    if (!shouldFetch || !url) {
      return;
    }
    
    let isCancelled = false;
    
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        
        if (!isCancelled) {
          setData(result);
        }
      } catch (error) {
        if (!isCancelled) {
          console.error('Fetch error:', error);
        }
      }
    };
    
    fetchData();
    
    return () => {
      isCancelled = true;
    };
  }, [shouldFetch, url]);
  
  return (
    <div>
      {data ? (
        <pre>{JSON.stringify(data, null, 2)}</pre>
      ) : (
        <div>No data</div>
      )}
    </div>
  );
}

useEffectの中で条件分岐することで、Hooksのルールを守りながら適切に処理できます。 クリーンアップ関数も使って、メモリリークを防いでいますね。

ESLintでルール違反を自動チェック

自動的にHooksのルール違反を検出する方法をご紹介します。 手動でチェックするのは大変なので、ツールに頼りましょう。

ESLint設定でエラーを防ごう

ESLintの設定を見てみましょう。

// .eslintrc.json
{
  "extends": [
    "react-app"
  ],
  "plugins": [
    "react-hooks"
  ],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

この設定により、以下のようなチェックが自動で行われます。

// ESLintが検出するルール違反の例
const eslintExamples = {
  "react-hooks/rules-of-hooks": {
    説明: "Hooksのルール違反を検出",
    検出例: [
      "条件分岐内でのHooks使用",
      "ループ内でのHooks使用",
      "通常の関数内でのHooks使用"
    ]
  },
  
  "react-hooks/exhaustive-deps": {
    説明: "useEffectの依存配列の不備を検出",
    検出例: [
      "依存配列に必要な値が含まれていない",
      "不要な値が依存配列に含まれている"
    ]
  }
};

実際のESLintエラー例

// 実際のESLintエラー例
function ProblematicComponent({ userId }) {
  const [user, setUser] = useState(null);
  
  // ESLint warning: exhaustive-deps
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, []); // userIdが依存配列に含まれていない
  
  return <div>{user?.name}</div>;
}

// 修正版
function FixedComponent({ userId }) {
  const [user, setUser] = useState(null);
  
  // ESLint警告が解消される
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]); // 依存配列に userId を追加
  
  return <div>{user?.name}</div>;
}

ESLintが自動でチェックしてくれるので、ルール違反を事前に防げます。

VS Codeでの設定

VS Codeでの設定例も見てみましょう。

// .vscode/settings.json
{
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ],
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "editor.formatOnSave": true
}

この設定により、ファイル保存時に自動でESLintの修正が適用されます。 とても便利ですよね。

実践的なベストプラクティス

Hooksを正しく使用するためのベストプラクティスをご紹介します。 これらを覚えておけば、より安全にReact開発ができますよ。

カスタムHookでロジックを整理しよう

関連するロジックをカスタムHookに分離すると、コードが整理されます。

// ✅ ベストプラクティス:関連するロジックをカスタムHookに分離
function useUserManagement() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const fetchUsers = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      
      const response = await fetch('/api/users');
      const data = await response.json();
      setUsers(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, []);
  
  const addUser = useCallback(async (userData) => {
    try {
      setLoading(true);
      
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData)
      });
      
      const newUser = await response.json();
      setUsers(prev => [...prev, newUser]);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, []);
  
  const deleteUser = useCallback(async (userId) => {
    try {
      setLoading(true);
      
      await fetch(`/api/users/${userId}`, { method: 'DELETE' });
      setUsers(prev => prev.filter(user => user.id !== userId));
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, []);
  
  useEffect(() => {
    fetchUsers();
  }, [fetchUsers]);
  
  return {
    users,
    loading,
    error,
    addUser,
    deleteUser,
    refetch: fetchUsers
  };
}

// カスタムHookを使用するコンポーネント
function UserManagementPage() {
  const { users, loading, error, addUser, deleteUser } = useUserManagement();
  const [showForm, setShowForm] = useState(false);
  
  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  
  return (
    <div>
      <h1>ユーザー管理</h1>
      
      <button onClick={() => setShowForm(!showForm)}>
        {showForm ? 'フォームを閉じる' : 'ユーザー追加'}
      </button>
      
      {showForm && <UserForm onSubmit={addUser} />}
      
      <UserList users={users} onDelete={deleteUser} />
    </div>
  );
}

カスタムHookを使うことで、ロジックが整理されて再利用しやすくなります。 コンポーネントもシンプルになって読みやすいですね。

メモ化でパフォーマンスを向上させよう

適切なメモ化により、パフォーマンスを向上させることができます。

// ✅ ベストプラクティス:適切なメモ化
function OptimizedComponent({ items, filter }) {
  // 計算コストの高い処理をuseMemoでメモ化
  const filteredItems = useMemo(() => {
    return items.filter(item => {
      return item.category.toLowerCase().includes(filter.toLowerCase());
    });
  }, [items, filter]);
  
  // 関数をuseCallbackでメモ化
  const handleItemClick = useCallback((itemId) => {
    console.log('Item clicked:', itemId);
    // 実際の処理
  }, []);
  
  return (
    <div>
      <h2>アイテム一覧 ({filteredItems.length}件)</h2>
      {filteredItems.map(item => (
        <OptimizedItem 
          key={item.id}
          item={item}
          onClick={handleItemClick}
        />
      ))}
    </div>
  );
}

// メモ化されたアイテムコンポーネント
const OptimizedItem = React.memo(({ item, onClick }) => {
  return (
    <div onClick={() => onClick(item.id)}>
      <h3>{item.name}</h3>
      <p>{item.description}</p>
    </div>
  );
});

useMemouseCallbackを適切に使うことで、不要な再計算や再レンダリングを防げます。 ただし、やりすぎは逆効果なので、本当に必要な場所だけに使いましょう。

まとめ:安全なHooks使用のポイント

React Hooksの2つの重要なルールについて詳しく解説しました。 これらのルールを守ることで、エラーに悩まされることなくReact開発ができますよ。

絶対に守るべき2つのルール

  • ルール1: Hooksはトップレベルでのみ呼び出す
  • ルール2: React関数コンポーネントまたはカスタムHookからのみ呼び出す

ルールを守るためのポイント

  • 条件分岐やループの前にHooksを宣言する
  • イベントハンドラーの中では状態更新関数のみ使用
  • カスタムHookはuseで始まる名前にする
  • ESLintでルール違反を自動チェック

開発をスムーズにするコツ

  • カスタムHookでロジックを整理
  • 適切なメモ化でパフォーマンス向上
  • エラーメッセージをしっかり読む

最初は基本的なルールから覚えて、徐々に応用テクニックを身につけていくのがおすすめです。

まずはシンプルなコンポーネントから始めて、Hooksのルールに慣れてください。 慣れてきたら、カスタムHookやメモ化などの高度なテクニックも活用してみましょう。

正しいHooksの使い方をマスターすることで、より安全で保守しやすいReactアプリケーションを開発できるようになりますよ。 ぜひ今回学んだ内容を実際のプロジェクトで活用してくださいね!

関連記事