What does useCallback do and how does it differ from useMemo?

👨‍💻 Frontend Developer 🟡 Often Asked 🎚️ Medium
#React #Hooks

Quick Answer

useCallback and useMemo — are React hooks for performance optimization through memoization:

  • useCallback memoizes a function
  • useMemo memoizes a computed value

Simple example of differences:

// useCallback returns a memoized function
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b]
);
 
// useMemo returns a memoized value
const memoizedValue = useMemo(
  () => computeExpensiveValue(a, b),
  [a, b]
);

Detailed Answer

Both hooks solve the problem of unnecessary re-renders and recalculations in React components, but are applied in different situations.

useCallback — Function Memoization

What it does

Returns a memoized version of a callback function that only changes when dependencies change:

function ParentComponent() {
  const [count, setCount] = useState(0);
  
  // Without useCallback: new function on every render
  const handleClick = () => {
    console.log(count);
  };
  
  // With useCallback: same function until count changes
  const memoizedHandleClick = useCallback(() => {
    console.log(count);
  }, [count]);
  
  return <ChildComponent onClick={memoizedHandleClick} />;
}

When to use

  1. Passing functions to memoized components:
const ExpensiveChild = React.memo(({ onClick }) => {
  console.log('Child rendered');
  return <button onClick={onClick}>Click me</button>;
});
 
function Parent() {
  const [count, setCount] = useState(0);
  
  // Without useCallback Child will re-render
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []); // Empty dependencies&nbsp;= function created once
  
  return (
    <>
      <ExpensiveChild onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
    </>
  );
}
  1. Dependency in other hooks:
function SearchComponent() {
  const [query, setQuery] = useState('');
  
  const search = useCallback((searchQuery) => {
    // Search logic
    console.log('Searching for:', searchQuery);
  }, []); // Function is&nbsp;not recreated
  
  // useEffect won't&nbsp;trigger on&nbsp;every render
  useEffect(() => {
    if (query) {
      search(query);
    }
  }, [query, search]);
  
  return <input onChange={(e) => setQuery(e.target.value)} />;
}

useMemo — Value Memoization

What it does

Returns a memoized value, recalculating it only when dependencies change:

function ExpensiveComponent({ items }) {
  // Without useMemo: recalculation on&nbsp;every render
  const expensiveValue = items.reduce((sum, item) => sum + item.value, 0);
  
  // With&nbsp;useMemo: recalculation only when items change
  const memoizedValue = useMemo(
    () => items.reduce((sum, item) => sum + item.value, 0),
    [items]
  );
  
  return <div>Total: {memoizedValue}</div>;
}

When to use

  1. Expensive computations:
function DataTable({ data, filter }) {
  const filteredData = useMemo(() => {
    console.log('Filtering data...');
    return data.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [data, filter]);
  
  return (
    <table>
      {filteredData.map(item => (
        <tr key={item.id}>
          <td>{item.name}</td>
        </tr>
      ))}
    </table>
  );
}
  1. Referential equality of objects:
function UserProfile({ userId }) {
  // New object on&nbsp;every render can trigger unnecessary effects
  const userConfig = useMemo(() => ({
    id: userId,
    theme: 'dark',
    permissions: ['read', 'write']
  }), [userId]);
  
  // useEffect will only trigger when userId changes
  useEffect(() => {
    loadUserData(userConfig);
  }, [userConfig]);
  
  return <div>User: {userId}</div>;
}

Key Differences

1. Type of returned value

// useCallback returns a&nbsp;function
const memoizedFn = useCallback(() => {
  return a + b;
}, [a, b]);
 
// useMemo returns the&nbsp;result of&nbsp;function execution
const memoizedResult = useMemo(() => {
  return a + b;
}, [a, b]);
 
console.log(typeof memoizedFn); // "function"
console.log(typeof memoizedResult); // "number"

2. Equivalence

// useCallback
const memoizedCallback = useCallback(
  () => doSomething(a, b),
  [a, b]
);
 
// Equivalent to&nbsp;useMemo returning a&nbsp;function
const memoizedCallback = useMemo(
  () => () => doSomething(a, b),
  [a, b]
);

Practical Examples

Form Example

function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  
  // Handler memoization
  const handleChange = useCallback((field) => (e) => {
    setFormData(prev => ({
      ...prev,
      [field]: e.target.value
    }));
  }, []); // Function created once
  
  // Validation memoization
  const isValid = useMemo(() => {
    return formData.name.length > 0 &&&nbsp;
           formData.email.includes('@') &&
           formData.message.length > 10;
  }, [formData]);
  
  return (
    <form>
      <input 
        value={formData.name} 
        onChange={handleChange('name')} 
      />
      <input 
        value={formData.email} 
        onChange={handleChange('email')} 
      />
      <textarea 
        value={formData.message} 
        onChange={handleChange('message')} 
      />
      <button disabled={!isValid}>
        Submit
      </button>
    </form>
  );
}

List Example

function TodoList({ todos, filter }) {
  // Filter memoization
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case 'active':
        return todos.filter(todo => !todo.completed);
      case 'completed':
        return todos.filter(todo => todo.completed);
      default:
        return todos;
    }
  }, [todos, filter]);
  
  // Handler memoization
  const toggleTodo = useCallback((id) => {
    // Todo toggle logic
  }, []);
  
  return (
    <ul>
      {filteredTodos.map(todo => (
        <TodoItem 
          key={todo.id} 
          todo={todo}
          onToggle={toggleTodo}
        />
      ))}
    </ul>
  );
}
 
const TodoItem = React.memo(({ todo, onToggle }) => {
  return (
    <li onClick={() => onToggle(todo.id)}>
      {todo.text}
    </li>
  );
});

Common Mistakes

1. Over-optimization

// ❌ Bad: simple calculations don't&nbsp;need memoization
const sum = useMemo(() => a + b, [a, b]);
 
// ✅ Good: just calculate
const sum = a + b;

2. Incorrect dependencies

// ❌ Bad: missing dependency
const calculate = useCallback(() => {
  return data.length * multiplier; // multiplier not&nbsp;in&nbsp;dependencies
}, [data]);
 
// ✅ Good: all external variables specified
const calculate = useCallback(() => {
  return data.length * multiplier;
}, [data, multiplier]);

3. Memoizing primitives

// ❌ Pointless: strings and&nbsp;numbers are&nbsp;already compared by&nbsp;value
const name = useMemo(() => 'John', []);
 
// ✅ Makes sense: objects are compared by&nbsp;reference
const config = useMemo(() => ({ name: 'John' }), []);

When to Use

useCallback is good for:

  • Passing functions to memoized components
  • Function dependencies in useEffect, useMemo
  • Preventing event handler recreation

useMemo is good for:

  • Expensive computations (sorting, filtering large arrays)
  • Creating objects/arrays used as dependencies
  • Optimizing child component rendering

Don’t use when:

  • Computations are simple and fast
  • Component rarely re-renders
  • No performance issues

Best Practices

  1. Measure performance before optimizing
  2. Start without memoization, add when necessary
  3. Use React DevTools Profiler to find bottlenecks
  4. Remember the cost of memoization itself
  5. Specify dependencies correctly

Remember: premature optimization — is the root of all evil. Use these hooks only when they actually solve performance problems.


Want more interview prep articles? Subscribe to EasyAdvice, bookmark the site, and improve every day 💪