Not using useCallback for performance optimization

Loading

Optimizing Event Handlers with useCallback

A common performance oversight in React is not using useCallback for event handlers, which can lead to unnecessary re-renders of child components.

The Problem: Unoptimized Handlers

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

  // ❌ New function created on every render
  const handleClick = () => {
    setCount(count + 1);
  };

  return <ChildComponent onClick={handleClick} />;
}

Why This Matters

  1. New function identity on each render
  2. Child components re-render unnecessarily
  3. Performance impact in large component trees

The Solution: useCallback

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

  // ✅ Memoized callback
  const handleClick = useCallback(() => {
    setCount(c => c + 1); // Using functional update
  }, []); // Empty dependencies = stable function

  return <ChildComponent onClick={handleClick} />;
}

When to Use useCallback

1. Passing Callbacks to Optimized Children

const MemoizedChild = React.memo(ChildComponent);

function Parent() {
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);

  return <MemoizedChild onClick={handleClick} />;
}

2. Event Handlers with Dependencies

function Search({ query }) {
  const [results, setResults] = useState([]);

  // ✅ Recreates only when query changes
  const search = useCallback(async () => {
    const data = await fetchResults(query);
    setResults(data);
  }, [query]);

  return (
    <>
      <button onClick={search}>Search</button>
      <ResultsList data={results} />
    </>
  );
}

3. In Dependency Arrays

useEffect(() => {
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, [handleResize]); // Needs stable function reference

When NOT to Use useCallback

  1. Simple components that rarely re-render
  2. Local event handlers not passed as props
  3. Cases where optimization costs outweigh benefits
function SimpleButton() {
  // ❌ Unnecessary - no child components
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);

  return <button onClick={handleClick}>Click</button>;
}

Advanced Patterns

1. Dynamic Handlers with Parameters

function ItemList({ items }) {
  const handleSelect = useCallback((itemId) => {
    console.log('Selected:', itemId);
  }, []);

  return items.map(item => (
    <Item key={item.id} item={item} onSelect={handleSelect} />
  ));
}

2. Combining with useMemo

function ComplexComponent({ users }) {
  const [query, setQuery] = useState('');

  const filteredUsers = useMemo(() => {
    return users.filter(u => u.name.includes(query));
  }, [users, query]);

  const handleSearch = useCallback((searchTerm) => {
    setQuery(searchTerm);
  }, []);

  return (
    <>
      <SearchBar onSearch={handleSearch} />
      <UserList users={filteredUsers} />
    </>
  );
}

Best Practices

  1. Profile first – Don’t optimize prematurely
  2. Use React.memo with useCallback for child components
  3. Keep dependencies accurate in the dependency array
  4. Consider context for deeply nested callbacks
  5. Use the functional update pattern when state depends on previous state

Remember that useCallback is a performance optimization tool – use it judiciously where it provides measurable benefits, not everywhere by default.

Leave a Reply

Your email address will not be published. Required fields are marked *