Optimizing Performance with useMemo and useCallback

Loading

React’s useMemo and useCallback hooks are essential tools for optimizing performance in React applications by preventing unnecessary computations and re-renders. Let’s explore how to use them effectively.

useMemo – Memoizing Expensive Calculations

Basic Usage

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

When to Use

  • For expensive calculations that don’t need to run on every render
  • When you need to maintain referential equality between renders

Real-World Examples

1. Filtering/Sorting Large Lists

function UserList({ users, searchTerm }) {
  const filteredUsers = useMemo(() => {
    return users.filter(user => 
      user.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }, [users, searchTerm]);

  return (
    <ul>
      {filteredUsers.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

2. Complex Object Transformations

function Dashboard({ metrics }) {
  const chartData = useMemo(() => {
    return {
      labels: metrics.map(m => m.date),
      datasets: [{
        data: metrics.map(m => m.value),
        // ... other complex chart config
      }]
    };
  }, [metrics]);

  return <Chart data={chartData} />;
}

3. Component Memoization

const ExpensiveComponent = React.memo(function({ complexConfig }) {
  // Renders based on complexConfig
});

function Parent({ props }) {
  const config = useMemo(() => {
    return { /* complex object creation */ };
  }, [props.dependency]);

  return <ExpensiveComponent complexConfig={config} />;
}

useCallback – Memoizing Functions

Basic Usage

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

When to Use

  • When passing callbacks to optimized child components that rely on reference equality
  • When functions are dependencies in other hooks like useEffect

Real-World Examples

1. Preventing Child Component Re-renders

const Child = React.memo(function({ onClick }) {
  // Only re-renders when onClick reference changes
});

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

  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // No dependencies = stable reference

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

2. Event Handlers with Dependencies

function ProductPage({ productId }) {
  const [quantity, setQuantity] = useState(1);

  const addToCart = useCallback(() => {
    api.addToCart(productId, quantity);
  }, [productId, quantity]); // Only recreates when these change

  return (
    <button onClick={addToCart}>
      Add to Cart
    </button>
  );
}

3. Dynamic List Rendering

function DynamicList({ items }) {
  const handleItemClick = useCallback((itemId) => {
    console.log('Item clicked:', itemId);
  }, []);

  return (
    <ul>
      {items.map(item => (
        <MemoizedListItem
          key={item.id}
          item={item}
          onClick={handleItemClick}
        />
      ))}
    </ul>
  );
}

Performance Optimization Patterns

1. Combining with React.memo

const ExpensiveChild = React.memo(function({ config, onClick }) {
  // Only re-renders when props change
});

function Parent() {
  const config = useMemo(() => ({ /* ... */ }), []);
  const onClick = useCallback(() => { /* ... */ }, []);

  return <ExpensiveChild config={config} onClick={onClick} />;
}

2. Context Value Optimization

const AppContext = createContext();

function AppProvider({ children }) {
  const [state, setState] = useState(initialState);

  const contextValue = useMemo(() => ({
    state,
    update: (newState) => setState(prev => ({ ...prev, ...newState }))
  }), [state]);

  return (
    <AppContext.Provider value={contextValue}>
      {children}
    </AppContext.Provider>
  );
}

3. Debouncing/Trottling Callbacks

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

  const debouncedSearch = useCallback(
    debounce(searchTerm => {
      api.search(searchTerm);
    }, 300),
    [] // debounce instance created once
  );

  useEffect(() => {
    debouncedSearch(query);
  }, [query, debouncedSearch]);

  return <input onChange={(e) => setQuery(e.target.value)} />;
}

Common Pitfalls and Best Practices

When NOT to Use

Overusing useMemo

// ❌ Unnecessary - simple calculations are cheap
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);

// ✅ Better
const fullName = `${firstName} ${lastName}`;

Empty Dependency Arrays

// ❌ Potentially stale closure
const handleSubmit = useCallback(() => {
  submitForm(values); // values might be stale
}, []);

// ✅ Include all dependencies
const handleSubmit = useCallback(() => {
  submitForm(values);
}, [values]);

Best Practices

  1. Profile First: Use React DevTools to identify real performance issues before optimizing
  2. Dependency Accuracy: Always include all values used in the callback/calculation
  3. Component Splitting: Often more effective than memoization
  4. Avoid Premature Optimization: Don’t memoize everything “just in case”
  5. TypeScript: Helps catch dependency array mistakes
// TypeScript can help catch missing dependencies
const handleSubmit = useCallback((newValue: string) => {
  setValues(prev => ({ ...prev, field: newValue }));
}, []); // TypeScript might warn about missing setValues dependency

Advanced Patterns

1. Custom Comparison for React.memo

const Item = React.memo(
  function Item({ item }) {
    return <div>{item.name}</div>;
  },
  (prevProps, nextProps) => {
    // Custom comparison - only re-render if id changed
    return prevProps.item.id === nextProps.item.id;
  }
);

2. Memoizing Multiple Values

function useUserSettings(userId) {
  const [settings, setSettings] = useState(null);
  const [preferences, setPreferences] = useState(null);

  return useMemo(() => ({
    settings,
    preferences,
    updateSettings: setSettings,
    updatePreferences: setPreferences
  }), [settings, preferences]);
}

3. Combining with useReducer

function TodoApp() {
  const [state, dispatch] = useReducer(reducer, initialState);

  // Stable dispatch reference
  const actions = useMemo(() => ({
    addTodo: text => dispatch({ type: 'ADD_TODO', text }),
    toggleTodo: id => dispatch({ type: 'TOGGLE_TODO', id })
  }), []);

  return <TodoList todos={state.todos} actions={actions} />;
}

Performance Measurement

function ExpensiveComponent() {
  const startTime = performance.now();

  // ... component render logic

  useEffect(() => {
    const endTime = performance.now();
    console.log(`Render took ${endTime - startTime}ms`);
  });

  return (
    // ... component JSX
  );
}

Remember that useMemo and useCallback are optimization tools that come with their own overhead. They should be used judiciously when you have measurable performance problems, not as a default for all computations and functions. Always profile your application to identify real bottlenecks before applying these optimizations.

Leave a Reply

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