React useCallbackとは?関数の再生成を防ぐ基本テクニック

React useCallbackフックの基本的な使い方から実践的な活用方法まで解説。パフォーマンス最適化のための関数メモ化テクニックを詳しく紹介

Learning Next 運営
45 分で読めます

みなさん、「Reactアプリが重い」「無駄な再レンダリングが多い」と感じたことはありませんか?

「コンポーネントが勝手に更新される」 「アプリの動作が遅くなってきた」 「パフォーマンス最適化って難しそう」

このような悩みを抱えたことはありませんか?

実は、これらの問題の多くは、関数の不要な再生成が原因なんです。 でも大丈夫です!useCallbackを使えば、簡単に解決できますよ。

この記事では、useCallbackの基本から実践的な使い方まで、初心者にも分かりやすく解説します。 一緒に、スムーズに動くReactアプリを作っていきましょう。

useCallbackって何?

useCallbackは、関数をメモ化するReactのフックです。

簡単に言うと、関数が毎回作り直されるのを防いでくれるんです。

問題:関数が毎回作られる

まず、問題のあるコードを見てみましょう。

// ❌ 問題のあるコード
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // この関数は毎回新しく作られる
  const handleClick = () => {
    console.log('Button clicked');
  };

  return (
    <div>
      <input 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
      />
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

const ChildComponent = React.memo(({ onClick }) => {
  console.log('ChildComponent rendered'); // 毎回実行される
  return <button onClick={onClick}>子コンポーネントのボタン</button>;
});

このコードでは、nameが変わるたびに、handleClick関数が新しく作られます。

そのため、ChildComponentReact.memoで囲まれていても、毎回再レンダリングされてしまうんです。

これが無駄な再レンダリングの原因なんですね。

解決策:useCallbackで関数を固定

useCallbackを使って修正してみましょう。

// ✅ useCallbackで最適化したコード
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // 関数を一度だけ作成
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []); // 依存配列が空なので、一度だけ作成

  return (
    <div>
      <input 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
      />
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

const ChildComponent = React.memo(({ onClick }) => {
  console.log('ChildComponent rendered'); // 初回のみ実行される
  return <button onClick={onClick}>子コンポーネントのボタン</button>;
});

これで、nameが変わってもChildComponentは再レンダリングされません。

handleClick関数が同じものを使い回されるからです。

useCallbackの基本的な書き方

useCallbackの構文を確認しましょう。

const memoizedCallback = useCallback(
  () => {
    // 実行したい処理
    doSomething(a, b);
  },
  [a, b] // 依存配列
);

第一引数:メモ化したい関数 第二引数:依存配列(この値が変わったときだけ関数を作り直す)

依存配列が空の場合は、一度だけ関数が作られます。

実用的な例

検索機能での使用例を見てみましょう。

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  // 検索関数をメモ化
  const handleSearch = useCallback(async (searchTerm) => {
    setLoading(true);
    try {
      const response = await fetch(`/api/search?q=${searchTerm}`);
      const data = await response.json();
      setResults(data);
    } catch (error) {
      console.error('Search failed:', error);
    } finally {
      setLoading(false);
    }
  }, []); // 依存するstateがないので空配列

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="検索キーワードを入力"
      />
      
      {loading && <p>検索中...</p>}
      
      <SearchResults 
        results={results} 
        onSearch={handleSearch} 
      />
    </div>
  );
}

この例では、handleSearch関数が一度だけ作られます。

そのため、SearchResultsコンポーネントの無駄な再レンダリングを防げます。

依存配列の使い方

useCallbackで最も重要なのが、依存配列の管理です。

正しく設定しないと、思わぬバグの原因になってしまいます。

依存配列とは?

依存配列は、どの値が変わったときに関数を作り直すかを指定します。

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // ❌ 間違った依存配列
  const fetchUserData = useCallback(async () => {
    const userData = await fetch(`/api/users/${userId}`).then(r => r.json());
    setUser(userData);
  }, []); // userIdが依存配列にない

この例では、userIdが変わっても関数が更新されません。

そのため、古いuserIdでAPIを呼び出してしまいます。

正しい依存配列の書き方

外部の値を使う場合は、必ず依存配列に含めましょう。

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // ✅ 正しい依存配列
  const fetchUserData = useCallback(async () => {
    const userData = await fetch(`/api/users/${userId}`).then(r => r.json());
    setUser(userData);
  }, [userId]); // userIdを依存配列に含める

  useEffect(() => {
    fetchUserData();
  }, [fetchUserData]);

  return (
    <div>
      {user && (
        <div>
          <h1>{user.name}</h1>
        </div>
      )}
    </div>
  );
}

これで、userIdが変わったときに新しい関数が作られ、正しいAPIが呼び出されます。

複数の依存値がある場合

複数の値に依存する場合の例です。

function PostList({ userId, category }) {
  const [posts, setPosts] = useState([]);

  const fetchPosts = useCallback(async (page = 1, limit = 10) => {
    const response = await fetch(
      `/api/users/${userId}/posts?category=${category}&page=${page}&limit=${limit}`
    );
    const data = await response.json();
    setPosts(data);
  }, [userId, category]); // 複数の値を依存配列に含める

  return (
    <div>
      <PostList 
        posts={posts} 
        onLoadMore={fetchPosts}
      />
    </div>
  );
}

userIdcategoryのどちらが変わっても、関数が作り直されます。

state更新の最適化

state更新関数は、関数型更新を使うと依存配列を空にできます。

function TodoApp() {
  const [todos, setTodos] = useState([]);

  // 関数型更新を使う
  const addTodo = useCallback((text) => {
    const newTodo = {
      id: Date.now(),
      text: text.trim(),
      completed: false
    };
    setTodos(prevTodos => [...prevTodos, newTodo]);
  }, []); // 依存配列が空

  // 普通の更新だと依存が必要
  const addTodoNormal = useCallback((text) => {
    const newTodo = {
      id: Date.now(),
      text: text.trim(),
      completed: false
    };
    setTodos([...todos, newTodo]); // todosに依存
  }, [todos]); // todosを依存配列に含める必要がある

  return (
    <div>
      <TodoInput onAdd={addTodo} />
    </div>
  );
}

関数型更新(prevTodos => [...prevTodos, newTodo])を使うと、現在のstateに依存しません。

そのため、依存配列を空にできて、より効率的になります。

実践的な使用例

実際のアプリでよく使われるパターンを見てみましょう。

Todoアプリの例

本格的なTodoアプリでのuseCallback活用例です。

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');

  // Todo追加
  const addTodo = useCallback((text) => {
    const newTodo = {
      id: Date.now(),
      text: text.trim(),
      completed: false,
      createdAt: new Date()
    };
    setTodos(prevTodos => [...prevTodos, newTodo]);
  }, []);

  // Todo完了切り替え
  const toggleTodo = useCallback((id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }, []);

  // Todo削除
  const deleteTodo = useCallback((id) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
  }, []);

  // Todo編集
  const editTodo = useCallback((id, newText) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, text: newText } : todo
      )
    );
  }, []);

  return (
    <div className="todo-app">
      <h1>Todo List</h1>
      
      <TodoInput onAdd={addTodo} />
      
      <div className="todo-controls">
        <select 
          value={filter} 
          onChange={(e) => setFilter(e.target.value)}
        >
          <option value="all">全て</option>
          <option value="active">未完了</option>
          <option value="completed">完了済み</option>
        </select>
      </div>

      <TodoList
        todos={todos}
        filter={filter}
        onToggle={toggleTodo}
        onDelete={deleteTodo}
        onEdit={editTodo}
      />
    </div>
  );
}

これらの関数は全てuseCallbackでメモ化されています。

そのため、フィルターが変わっても、各TodoアイテムのHandler関数は変わりません。

結果的に、不要な再レンダリングを防げます。

最適化された子コンポーネント

子コンポーネントもReact.memoで最適化しましょう。

const TodoInput = React.memo(({ onAdd }) => {
  const [text, setText] = useState('');

  const handleSubmit = useCallback((e) => {
    e.preventDefault();
    if (text.trim()) {
      onAdd(text);
      setText('');
    }
  }, [text, onAdd]);

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="新しいタスクを入力"
      />
      <button type="submit">追加</button>
    </form>
  );
});

const TodoItem = React.memo(({ todo, onToggle, onDelete, onEdit }) => {
  const [isEditing, setIsEditing] = useState(false);
  const [editText, setEditText] = useState(todo.text);

  const handleSave = useCallback(() => {
    if (editText.trim()) {
      onEdit(todo.id, editText);
      setIsEditing(false);
    }
  }, [todo.id, editText, onEdit]);

  const handleCancel = useCallback(() => {
    setEditText(todo.text);
    setIsEditing(false);
  }, [todo.text]);

  return (
    <li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      
      {isEditing ? (
        <div className="edit-mode">
          <input
            value={editText}
            onChange={(e) => setEditText(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === 'Enter') handleSave();
              if (e.key === 'Escape') handleCancel();
            }}
            autoFocus
          />
          <button onClick={handleSave}>保存</button>
          <button onClick={handleCancel}>キャンセル</button>
        </div>
      ) : (
        <div className="view-mode">
          <span 
            onDoubleClick={() => setIsEditing(true)}
            className="todo-text"
          >
            {todo.text}
          </span>
          <button onClick={() => setIsEditing(true)}>編集</button>
          <button onClick={() => onDelete(todo.id)}>削除</button>
        </div>
      )}
    </li>
  );
});

React.memoとuseCallbackを組み合わせることで、最大限の最適化効果が得られます。

API呼び出しの最適化

データ取得機能での使用例です。

function UserManagement() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // ユーザー一覧取得
  const fetchUsers = useCallback(async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch('/api/users');
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const data = await response.json();
      setUsers(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, []);

  // ユーザー更新
  const updateUser = useCallback(async (id, updates) => {
    try {
      const response = await fetch(`/api/users/${id}`, {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(updates),
      });

      if (!response.ok) {
        throw new Error(`Update failed: ${response.status}`);
      }

      const updatedUser = await response.json();
      
      // ローカルstateを更新
      setUsers(prevUsers => 
        prevUsers.map(user => 
          user.id === id ? { ...user, ...updatedUser } : user
        )
      );
      
    } catch (error) {
      setError(error.message);
    }
  }, []);

  // ユーザー削除
  const deleteUser = useCallback(async (id) => {
    try {
      const response = await fetch(`/api/users/${id}`, {
        method: 'DELETE',
      });

      if (!response.ok) {
        throw new Error(`Delete failed: ${response.status}`);
      }

      // ローカルstateから削除
      setUsers(prevUsers => prevUsers.filter(user => user.id !== id));
      
    } catch (error) {
      setError(error.message);
    }
  }, []);

  // 初回データ取得
  useEffect(() => {
    fetchUsers();
  }, [fetchUsers]);

  return (
    <div>
      <h1>ユーザー管理</h1>
      
      {loading && <p>読み込み中...</p>}
      {error && <p>エラー: {error}</p>}
      
      {users && (
        <UserList 
          users={users} 
          onUpdate={updateUser}
          onDelete={deleteUser}
        />
      )}
    </div>
  );
}

API呼び出し関数をメモ化することで、useEffectの依存配列が安定します。

これにより、不要なAPI呼び出しを防げます。

カスタムフックでの活用

useCallbackは、カスタムフックでも威力を発揮します。

ローカルストレージとの同期

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  // 値を設定する関数をメモ化
  const setValue = useCallback((value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  }, [key, storedValue]);

  // 値を削除する関数をメモ化
  const removeValue = useCallback(() => {
    try {
      window.localStorage.removeItem(key);
      setStoredValue(initialValue);
    } catch (error) {
      console.error(`Error removing localStorage key "${key}":`, error);
    }
  }, [key, initialValue]);

  return [storedValue, setValue, removeValue];
}

このカスタムフックを使うと、ローカルストレージとの同期が簡単になります。

function UserSettings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [language, setLanguage] = useLocalStorage('language', 'ja');

  return (
    <div>
      <h2>設定</h2>
      
      <div>
        <label>テーマ:</label>
        <select value={theme} onChange={(e) => setTheme(e.target.value)}>
          <option value="light">ライト</option>
          <option value="dark">ダーク</option>
        </select>
      </div>
      
      <div>
        <label>言語:</label>
        <select value={language} onChange={(e) => setLanguage(e.target.value)}>
          <option value="ja">日本語</option>
          <option value="en">English</option>
        </select>
      </div>
    </div>
  );
}

設定の変更が自動的にローカルストレージに保存されます。

デバウンス機能

入力値の変更を遅延させるデバウンス機能です。

function useDebounce(callback, delay) {
  const callbackRef = useRef(callback);
  const timeoutRef = useRef();

  // 最新のコールバックを保持
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // デバウンスされた関数をメモ化
  const debouncedCallback = useCallback(
    (...args) => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
      
      timeoutRef.current = setTimeout(() => {
        callbackRef.current(...args);
      }, delay);
    },
    [delay]
  );

  // クリーンアップ
  useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);

  return debouncedCallback;
}

検索機能で使ってみましょう。

function SearchInput({ onSearch }) {
  const [query, setQuery] = useState('');

  // デバウンス付きの検索
  const debouncedSearch = useDebounce((searchTerm) => {
    if (searchTerm.length > 2) {
      onSearch(searchTerm);
    }
  }, 300);

  const handleInputChange = useCallback((e) => {
    const value = e.target.value;
    setQuery(value);
    debouncedSearch(value);
  }, [debouncedSearch]);

  return (
    <input
      type="text"
      value={query}
      onChange={handleInputChange}
      placeholder="検索キーワードを入力"
    />
  );
}

これで、入力停止から300ミリ秒後に検索が実行されます。

連続入力時の無駄なAPI呼び出しを防げます。

React.memoとの組み合わせ

useCallbackは、React.memoと組み合わせることで真価を発揮します。

大量データを扱うリスト

function LargeList({ items, onItemClick, onItemDelete }) {
  // フィルタリングとソートをメモ化
  const processedItems = useMemo(() => {
    return items.sort((a, b) => {
      return a.name.localeCompare(b.name);
    });
  }, [items]);

  // アイテムクリックハンドラをメモ化
  const handleItemClick = useCallback((item) => {
    onItemClick(item.id, item);
  }, [onItemClick]);

  // アイテム削除ハンドラをメモ化
  const handleItemDelete = useCallback((itemId) => {
    if (confirm('本当に削除しますか?')) {
      onItemDelete(itemId);
    }
  }, [onItemDelete]);

  return (
    <div className="large-list">
      <div className="list-stats">
        表示中: {processedItems.length}件
      </div>
      
      <div className="list-container">
        {processedItems.map(item => (
          <ListItem
            key={item.id}
            item={item}
            onClick={handleItemClick}
            onDelete={handleItemDelete}
          />
        ))}
      </div>
    </div>
  );
}

子コンポーネントもメモ化します。

const ListItem = React.memo(({ item, onClick, onDelete }) => {
  console.log(`Rendering item: ${item.name}`); // デバッグ用

  // アイテム固有のクリックハンドラ
  const handleClick = useCallback(() => {
    onClick(item);
  }, [item, onClick]);

  // アイテム固有の削除ハンドラ
  const handleDelete = useCallback((e) => {
    e.stopPropagation();
    onDelete(item.id);
  }, [item.id, onDelete]);

  return (
    <div className="list-item" onClick={handleClick}>
      <div className="item-info">
        <h3>{item.name}</h3>
        <p>カテゴリ: {item.category}</p>
        <p>作成日: {new Date(item.createdAt).toLocaleDateString()}</p>
      </div>
      
      <div className="item-actions">
        <button onClick={handleDelete}>削除</button>
      </div>
    </div>
  );
});

この組み合わせにより、アイテムの変更がない限り、各リストアイテムは再レンダリングされません。

大量のデータを扱う場合でも、スムーズに動作します。

使用例

function App() {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(true);

  // データの読み込み
  useEffect(() => {
    const loadData = async () => {
      setLoading(true);
      try {
        // 大量のデータを生成
        const data = Array.from({ length: 1000 }, (_, index) => ({
          id: index + 1,
          name: `アイテム ${index + 1}`,
          category: ['カテゴリA', 'カテゴリB', 'カテゴリC'][index % 3],
          createdAt: new Date(Date.now() - Math.random() * 10000000000).toISOString(),
        }));
        setItems(data);
      } catch (error) {
        console.error('データの読み込みに失敗しました:', error);
      } finally {
        setLoading(false);
      }
    };

    loadData();
  }, []);

  // アイテムクリックハンドラ
  const handleItemClick = useCallback((itemId, item) => {
    console.log('Item clicked:', item);
  }, []);

  // アイテム削除ハンドラ
  const handleItemDelete = useCallback((itemId) => {
    setItems(prevItems => prevItems.filter(item => item.id !== itemId));
  }, []);

  if (loading) {
    return <div>データを読み込み中...</div>;
  }

  return (
    <div className="app">
      <h1>大規模リストアプリ</h1>

      <LargeList
        items={items}
        onItemClick={handleItemClick}
        onItemDelete={handleItemDelete}
      />
    </div>
  );
}

1000件のアイテムでも、スムーズに動作します。

useCallbackとReact.memoの威力を実感できるはずです。

よくある間違いと対策

useCallbackを使う際に、よくある間違いをご紹介します。

過度な最適化

// ❌ 効果が薄い最適化
function SimpleComponent() {
  const [count, setCount] = useState(0);

  // これは不要
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);

  // これも不要
  const handleIncrement = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleIncrement}>+1</button>
      <button onClick={handleClick}>Log</button>
    </div>
  );
}

単純な関数や、子コンポーネントに渡さない関数は、メモ化不要です。

// ✅ 適切な最適化
function OptimizedComponent() {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState([]);

  // 子コンポーネントに渡すのでメモ化が有効
  const handleItemAdd = useCallback((newItem) => {
    setItems(prev => [...prev, newItem]);
  }, []);

  // 単純なstate更新は最適化不要
  const handleIncrement = () => {
    setCount(c => c + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleIncrement}>+1</button>
      
      <ItemList 
        items={items}
        onAdd={handleItemAdd}
      />
    </div>
  );
}

メモ化すべき関数:子コンポーネントに渡される関数 メモ化不要の関数:コンポーネント内でのみ使用される単純な関数

依存配列の間違い

// ❌ 依存配列の間違い
function ProblematicComponent({ userId, apiEndpoint }) {
  const [userData, setUserData] = useState(null);

  // 問題1: 必要な値が依存配列にない
  const fetchUser = useCallback(async () => {
    const response = await fetch(`${apiEndpoint}/users/${userId}`);
    const data = await response.json();
    setUserData(data);
  }, []); // userIdとapiEndpointが依存配列にない

  // 問題2: 不安定な依存値
  const processData = useCallback((data) => {
    return data.map(item => ({
      ...item,
      timestamp: new Date() // 毎回新しい日付
    }));
  }, [new Date()]); // 毎回新しいDateオブジェクト

  return <div>{/* JSX */}</div>;
}

これらの問題を修正してみましょう。

// ✅ 正しい依存配列
function FixedComponent({ userId, apiEndpoint }) {
  const [userData, setUserData] = useState(null);

  // 修正1: 必要な依存値をすべて含める
  const fetchUser = useCallback(async () => {
    const response = await fetch(`${apiEndpoint}/users/${userId}`);
    const data = await response.json();
    setUserData(data);
  }, [userId, apiEndpoint]);

  // 修正2: 安定した値を使用
  const processData = useCallback((data) => {
    return data.map(item => ({
      ...item,
      processedAt: Date.now() // 関数実行時の時刻
    }));
  }, []); // 外部依存なし

  return <div>{/* JSX */}</div>;
}

ポイント

  • 関数内で使用する外部の値は、必ず依存配列に含める
  • 毎回変わる値(new Date()など)は依存配列に入れない
  • ESLintのルールexhaustive-depsを有効にすると、間違いを防げる

メモリリーク対策

非同期処理では、コンポーネントのアンマウント時のクリーンアップが重要です。

// ❌ メモリリークの可能性
function LeakyComponent() {
  const [data, setData] = useState(null);

  const fetchData = useCallback(async () => {
    const response = await fetch('/api/data');
    const result = await response.json();
    setData(result); // コンポーネントがアンマウントされても実行される
  }, []);

  return <div>{/* JSX */}</div>;
}

AbortControllerを使ってクリーンアップしましょう。

// ✅ メモリリーク対策済み
function SafeComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  const fetchData = useCallback(async () => {
    const controller = new AbortController();
    setLoading(true);

    try {
      const response = await fetch('/api/data', {
        signal: controller.signal
      });
      const result = await response.json();
      
      // アンマウントされていないかチェック
      if (!controller.signal.aborted) {
        setData(result);
      }
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('Fetch error:', error);
      }
    } finally {
      if (!controller.signal.aborted) {
        setLoading(false);
      }
    }

    // クリーンアップ関数を返す
    return () => controller.abort();
  }, []);

  useEffect(() => {
    const cleanup = fetchData();
    return cleanup;
  }, [fetchData]);

  return <div>{loading ? 'Loading...' : /* JSX */}</div>;
}

これで、コンポーネントがアンマウントされても安全です。

まとめ

useCallbackは、Reactアプリのパフォーマンス向上に欠かせないツールです。

useCallbackの効果的な使い方

  • 子コンポーネントに渡す関数をメモ化する
  • 依存配列を正しく管理する
  • React.memoと組み合わせる
  • 過度な最適化は避ける

基本パターン

// 基本形
const memoizedCallback = useCallback(() => {
  doSomething();
}, []);

// 依存値あり
const memoizedCallback = useCallback((param) => {
  doSomething(param, externalValue);
}, [externalValue]);

// 状態更新
const updateState = useCallback((newValue) => {
  setState(prevState => ({ ...prevState, ...newValue }));
}, []);

注意すべきポイント

  • すべての関数をメモ化する必要はない
  • 依存配列には使用する外部の値を全て含める
  • 非同期処理にはクリーンアップを実装する

パフォーマンス向上のコツ

  1. 測定してから最適化:まずは実際のボトルネックを特定
  2. 段階的な適用:効果的な部分から順番に最適化
  3. 継続的な見直し:アプリの成長に合わせて調整

useCallbackを正しく使うことで、ユーザーにとって快適なReactアプリを作ることができます。

ぜひ今回学んだテクニックを、あなたのプロジェクトで試してみてください。 きっと、アプリの動作が軽やかになることを実感できるはずです!

関連記事