useMemo and useCallback Hooks

Optimizing Values and Functions with React's Memoization Hooks

Introduction to useMemo and useCallback

In our previous lecture, we explored how React.memo can prevent unnecessary component re-renders. However, React.memo only works effectively when the component's props maintain stable references across renders. This is where useMemo and useCallback come into play.

These hooks help solve two common performance issues in React applications:

flowchart TD Component[Component Render] --> Props[Props Change Check] Props -->|"New Objects/Functions"| ReactMemo[React.memo] ReactMemo -->|"Props 'Changed'"| Rerender[Component Re-renders] Component --> |Use| UseMemo[useMemo] Component --> |Use| UseCallback[useCallback] UseMemo -->|"Stable Objects"| Props UseCallback -->|"Stable Functions"| Props classDef hook fill:#9af,stroke:#36a,stroke-width:2px; classDef problem fill:#f99,stroke:#933,stroke-width:2px; classDef solution fill:#9f9,stroke:#393,stroke-width:2px; class UseMemo,UseCallback hook; class Rerender problem; class Props solution;

Let's understand what each hook does:

Real-World Analogy: Caching vs. Recalculating

Imagine you're a math teacher grading tests:

  • Without memoization: You recalculate every student's test score from scratch each time you need to refer to it
  • With memoization: You calculate each score once, write it down, and simply refer to your notes when you need the score again

Just as it would be inefficient to recalculate every score repeatedly, React applications can waste resources by recomputing values or recreating functions unnecessarily.

The useMemo Hook

The useMemo hook memoizes the result of a computation, only recomputing when one of its dependencies changes.

Basic Syntax


const memoizedValue = useMemo(() => {
  // Perform expensive calculation
  return computeExpensiveValue(a, b);
}, [a, b]); // Dependencies array
        

The useMemo hook takes two arguments:

  1. A create function that computes a value
  2. A dependencies array that determines when to recompute the value

During the initial render, useMemo calls the create function and returns its result. On subsequent renders, it will either return the cached result (if dependencies haven't changed) or recompute and cache a new result (if dependencies have changed).

Memoizing Expensive Calculations

The primary use case for useMemo is to avoid repeating expensive calculations when unnecessary:


import React, { useState, useMemo } from 'react';

function ExpensiveCalculation({ data, threshold }) {
  // This calculation will only run when data or threshold changes
  const processedData = useMemo(() => {
    console.log('Processing data...');
    
    // Simulate expensive calculation
    return data.filter(item => {
      // Imagine this is a complex calculation
      let result = 0;
      for (let i = 0; i < 10000; i++) {
        result += Math.sqrt(item.value * i);
      }
      return result > threshold;
    });
  }, [data, threshold]);
  
  return (
    <div>
      <h2>Processed Data</h2>
      <p>Found {processedData.length} items above threshold.</p>
      <ul>
        {processedData.map(item => (
          <li key={item.id}>
            {item.name}: {item.value}
          </li>
        ))}
      </ul>
    </div>
  );
}
        

Memoizing Objects to Stabilize Props

Another common use for useMemo is to prevent new object references from causing unnecessary re-renders of memoized child components:


function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  // Fetch user data when userId changes
  useEffect(() => {
    setLoading(true);
    fetchUser(userId).then(data => {
      setUser(data);
      setLoading(false);
    });
  }, [userId]);
  
  // Create a stable userInfo object that only changes when user changes
  const userInfo = useMemo(() => {
    if (!user) return null;
    
    return {
      displayName: `${user.firstName} ${user.lastName}`,
      email: user.email,
      role: user.role,
      joinDate: new Date(user.joinDate).toLocaleDateString()
    };
  }, [user]);
  
  if (loading) return <div>Loading...</div>;
  if (!user) return <div>User not found</div>;
  
  return (
    <div>
      {/* UserInfo will only re-render when the userInfo object changes */}
      <UserInfo info={userInfo} />
      <UserActivity userId={userId} />
    </div>
  );
}

// Memoized child component
const UserInfo = React.memo(function UserInfo({ info }) {
  console.log('UserInfo rendered');
  
  if (!info) return null;
  
  return (
    <div className="user-info">
      <h2>{info.displayName}</h2>
      <p>Email: {info.email}</p>
      <p>Role: {info.role}</p>
      <p>Joined: {info.joinDate}</p>
    </div>
  );
});
        

Real-World Example: Data Visualization

Data visualization components often perform complex calculations to transform raw data into visualizable formats. Using useMemo can significantly improve performance:


function DataVisualization({ rawData, filters, dimensions }) {
  // Memoize the complex data processing
  const processedData = useMemo(() => {
    console.log('Processing visualization data...');
    
    // 1. Filter data based on criteria
    const filteredData = rawData.filter(item => {
      return Object.entries(filters).every(([key, value]) => {
        // Skip empty filters
        if (!value) return true;
        return item[key] === value;
      });
    });
    
    // 2. Group and aggregate data
    const groupedData = filteredData.reduce((acc, item) => {
      const key = item.category;
      if (!acc[key]) {
        acc[key] = {
          category: key,
          count: 0,
          total: 0,
          items: []
        };
      }
      
      acc[key].count += 1;
      acc[key].total += item.value;
      acc[key].items.push(item);
      
      return acc;
    }, {});
    
    // 3. Convert to array and calculate averages
    const result = Object.values(groupedData).map(group => ({
      ...group,
      average: group.total / group.count
    }));
    
    // 4. Sort by specified criteria
    return result.sort((a, b) => b.total - a.total);
  }, [rawData, filters]); // Only recalculate when data or filters change
  
  // Memoize chart configuration
  const chartConfig = useMemo(() => {
    return {
      width: dimensions.width,
      height: dimensions.height,
      margin: { top: 20, right: 30, bottom: 40, left: 50 },
      colors: ['#ff6b6b', '#48dbfb', '#1dd1a1', '#5f27cd']
    };
  }, [dimensions]); // Only recalculate when dimensions change
  
  return (
    <div className="visualization-container">
      <h2>Data Visualization</h2>
      
      <div className="chart-area">
        <BarChart 
          data={processedData} 
          config={chartConfig} 
        />
      </div>
      
      <div className="data-summary">
        <h3>Summary</h3>
        <p>Total categories: {processedData.length}</p>
        <p>Total items: {processedData.reduce((sum, group) => sum + group.count, 0)}</p>
        <p>Highest value: {processedData[0]?.total || 0}</p>
      </div>
    </div>
  );
}

// Memoized chart component
const BarChart = React.memo(function BarChart({ data, config }) {
  console.log('BarChart rendering');
  
  // Chart rendering logic...
  
  return <svg width={config.width} height={config.height}>{/* Chart elements */}</svg>;
});
          

In this example, both the data processing and chart configuration are memoized to prevent unnecessary recalculations when the component re-renders due to unrelated state changes.

The useCallback Hook

The useCallback hook memoizes a function definition, only creating a new function when dependencies change.

Basic Syntax


const memoizedCallback = useCallback(() => {
  // Function logic
  doSomething(a, b);
}, [a, b]); // Dependencies array
        

The useCallback hook takes two arguments:

  1. A function to memoize
  2. A dependencies array that determines when to recreate the function

The key difference between useMemo and useCallback is:


// These are equivalent:
const memoizedFunction = useCallback(() => {
  return doSomething(a, b);
}, [a, b]);

const memoizedFunction = useMemo(() => {
  return () => {
    return doSomething(a, b);
  };
}, [a, b]);
        

Stabilizing Event Handlers

The primary use case for useCallback is to prevent new function instances from causing child component re-renders:


function TodoList() {
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState('');
  
  // This function will be recreated only when setTodos changes
  // (which never happens since it's from useState)
  const handleAddTodo = useCallback(() => {
    if (newTodo.trim()) {
      setTodos([...todos, { id: Date.now(), text: newTodo, completed: false }]);
      setNewTodo('');
    }
  }, [newTodo, todos, setTodos]);
  
  // This function will be recreated only when todos changes
  const handleToggleTodo = useCallback((id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, [todos]);
  
  // This function will be recreated only when todos changes
  const handleDeleteTodo = useCallback((id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  }, [todos]);
  
  return (
    <div>
      <h2>Todo List</h2>
      
      <div>
        <input
          type="text"
          value={newTodo}
          onChange={e => setNewTodo(e.target.value)}
          placeholder="Add a new todo"
        />
        <button onClick={handleAddTodo}>Add</button>
      </div>
      
      <ul>
        {todos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={handleToggleTodo}
            onDelete={handleDeleteTodo}
          />
        ))}
      </ul>
    </div>
  );
}

// Memoized todo item to prevent unnecessary re-renders
const TodoItem = React.memo(function TodoItem({ todo, onToggle, onDelete }) {
  console.log(`TodoItem rendered: ${todo.text}`);
  
  return (
    <li style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
});
        

Optimizing Event Handler Parameters

A common pattern is to define handlers that take parameters. useCallback helps maintain stable references:


function ShoppingCart({ items, onUpdateQuantity, onRemoveItem }) {
  return (
    <div className="shopping-cart">
      {items.map(item => (
        <CartItem
          key={item.id}
          item={item}
          onUpdateQuantity={onUpdateQuantity}
          onRemoveItem={onRemoveItem}
        />
      ))}
    </div>
  );
}

function CartPage() {
  const [cart, setCart] = useState([
    { id: 1, name: 'Product 1', price: 10, quantity: 1 },
    { id: 2, name: 'Product 2', price: 20, quantity: 2 }
  ]);
  
  // Memoize the handlers to maintain stable references
  const handleUpdateQuantity = useCallback((itemId, newQuantity) => {
    setCart(prevCart =>
      prevCart.map(item =>
        item.id === itemId
          ? { ...item, quantity: Math.max(1, newQuantity) }
          : item
      )
    );
  }, []);
  
  const handleRemoveItem = useCallback((itemId) => {
    setCart(prevCart => prevCart.filter(item => item.id !== itemId));
  }, []);
  
  // Derived value can be memoized with useMemo
  const cartTotal = useMemo(() => {
    return cart.reduce((total, item) => total + (item.price * item.quantity), 0);
  }, [cart]);
  
  return (
    <div>
      <h1>Shopping Cart</h1>
      
      <ShoppingCart
        items={cart}
        onUpdateQuantity={handleUpdateQuantity}
        onRemoveItem={handleRemoveItem}
      />
      
      <div className="cart-summary">
        <h2>Summary</h2>
        <p>Total Items: {cart.length}</p>
        <p>Total Quantity: {cart.reduce((sum, item) => sum + item.quantity, 0)}</p>
        <p>Total Price: ${cartTotal.toFixed(2)}</p>
        <button>Proceed to Checkout</button>
      </div>
    </div>
  );
}

// Memoized cart item component
const CartItem = React.memo(function CartItem({ item, onUpdateQuantity, onRemoveItem }) {
  console.log(`CartItem rendered: ${item.name}`);
  
  const handleIncrement = () => onUpdateQuantity(item.id, item.quantity + 1);
  const handleDecrement = () => onUpdateQuantity(item.id, item.quantity - 1);
  const handleRemove = () => onRemoveItem(item.id);
  
  return (
    <div className="cart-item">
      <div>{item.name}</div>
      <div>${item.price.toFixed(2)}</div>
      <div className="quantity-controls">
        <button onClick={handleDecrement}>-</button>
        <span>{item.quantity}</span>
        <button onClick={handleIncrement}>+</button>
      </div>
      <div>${(item.price * item.quantity).toFixed(2)}</div>
      <button onClick={handleRemove}>Remove</button>
    </div>
  );
});
        

Functional Updates with useState

Notice in the previous example how we used the functional form of setCart:


// Functional update pattern
setCart(prevCart => prevCart.map(item => ... ));
          

This pattern is important when using useCallback with dependencies. By using the functional update form, you can often remove dependencies from the dependency array, making your callbacks more stable:


// Without functional updates - requires 'cart' dependency
const handleUpdateQuantity = useCallback((itemId, newQuantity) => {
  setCart(cart.map(item =>
    item.id === itemId
      ? { ...item, quantity: Math.max(1, newQuantity) }
      : item
  ));
}, [cart]); // cart must be a dependency

// With functional updates - no 'cart' dependency needed
const handleUpdateQuantity = useCallback((itemId, newQuantity) => {
  setCart(prevCart => prevCart.map(item =>
    item.id === itemId
      ? { ...item, quantity: Math.max(1, newQuantity) }
      : item
  ));
}, []); // No dependencies!
          

This creates a more stable function reference that doesn't need to be recreated whenever the cart changes.

Dependencies and Optimization

Managing dependencies correctly is crucial for both useMemo and useCallback. Let's explore some best practices:

Understanding the Dependencies Array

The dependencies array works the same way as in useEffect:


// Compute once (on mount)
const value1 = useMemo(() => expensiveComputation(), []);

// Compute on every render (not recommended)
const value2 = useMemo(() => expensiveComputation());

// Compute when dependencies change
const value3 = useMemo(() => expensiveComputation(a, b), [a, b]);
        

Dependency Pitfalls

Common mistakes with dependencies can lead to unexpected behavior:


// Mistake 1: Missing dependencies
const calculateTotal = useCallback(() => {
  return cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}, []); // ❌ Missing 'cart' dependency

// Mistake 2: Unnecessary dependencies
const formatDate = useCallback((date) => {
  return new Date(date).toLocaleDateString();
}, [someUnrelatedState]); // ❌ Unnecessary dependency

// Mistake 3: Objects/arrays created inline
const sortItems = useMemo(() => {
  return (items) => {
    return [...items].sort((a, b) => a.name.localeCompare(b.name));
  };
}, [{ key: 'value' }]); // ❌ New object on every render
        

Dependency Deep Dives

Let's fix the above issues:


// Fix 1: Add missing dependencies
const calculateTotal = useCallback(() => {
  return cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}, [cart.items]); // ✅ Include all dependencies

// Better solution: Use functional updates when possible
const handleAddItem = useCallback((newItem) => {
  setCart(prevCart => ({
    ...prevCart,
    items: [...prevCart.items, newItem]
  }));
}, []); // ✅ No dependencies needed

// Fix 2: Remove unnecessary dependencies
const formatDate = useCallback((date) => {
  return new Date(date).toLocaleDateString();
}, []); // ✅ No external dependencies

// Fix 3: Move object creation outside or memoize it
const sortConfig = useMemo(() => ({ key: 'value' }), []);
const sortItems = useMemo(() => {
  return (items) => {
    return [...items].sort((a, b) => a.name.localeCompare(b.name));
  };
}, [sortConfig]); // ✅ Stable dependency
        

When to Omit Memoization

Sometimes memoization isn't necessary:


// Don't need useMemo for simple calculations
const total = items.reduce((sum, item) => sum + item.price, 0);

// Better than useMemo for simple transformations
const upperName = name.toUpperCase();

// Don't need useCallback for simple event handlers in non-optimized components
function SimpleButton() {
  const handleClick = () => {
    console.log('Button clicked');
  };
  
  return <button onClick={handleClick}>Click Me</button>;
}
        

The ESLint Hooks Plugin

The ESLint plugin for React hooks (eslint-plugin-react-hooks) includes rules to help ensure correct dependencies:

  • react-hooks/exhaustive-deps: Warns when dependencies are missing

This plugin is included in Create React App and many other React setups. It's a good practice to enable it and pay attention to its warnings.


// eslintrc.js configuration
module.exports = {
  plugins: [
    'react-hooks'
  ],
  rules: {
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn'
  }
};
          

The plugin will catch issues like:


// This will trigger a warning
const handleSubmit = useCallback(() => {
  console.log(formData); // formData is used but not in dependencies
}, []); // ESLint will warn about missing 'formData' dependency
          

Advanced Patterns with useMemo and useCallback

Recursive Memoization

For complex components, you might need to memoize at multiple levels:


function ComplexForm({ initialData, onSubmit }) {
  const [formData, setFormData] = useState(initialData);
  
  // Memoize the event handlers
  const handleChange = useCallback((e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  }, []);
  
  const handleSubmit = useCallback((e) => {
    e.preventDefault();
    onSubmit(formData);
  }, [formData, onSubmit]);
  
  // Memoize derived/computed data
  const isFormValid = useMemo(() => {
    return Object.entries(formData).every(([key, value]) => {
      if (key === 'email') {
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
      }
      return value.trim() !== '';
    });
  }, [formData]);
  
  // Memoize the form configuration
  const fieldConfig = useMemo(() => ({
    name: {
      label: 'Full Name',
      placeholder: 'Enter your full name',
      required: true
    },
    email: {
      label: 'Email Address',
      placeholder: 'Enter your email',
      type: 'email',
      required: true
    },
    message: {
      label: 'Message',
      placeholder: 'Enter your message',
      component: 'textarea',
      required: true
    }
  }), []);
  
  // Memoize the fields component
  const Fields = useMemo(() => {
    return Object.entries(fieldConfig).map(([name, config]) => (
      <FormField
        key={name}
        name={name}
        value={formData[name] || ''}
        onChange={handleChange}
        {...config}
      />
    ));
  }, [fieldConfig, formData, handleChange]);
  
  return (
    <form onSubmit={handleSubmit}>
      {Fields}
      <button type="submit" disabled={!isFormValid}>
        Submit
      </button>
    </form>
  );
}

// Memoized form field component
const FormField = React.memo(function FormField({
  name,
  label,
  value,
  onChange,
  component = 'input',
  ...rest
}) {
  console.log(`Rendering field: ${name}`);
  
  const inputProps = {
    id: name,
    name,
    value,
    onChange,
    ...rest
  };
  
  return (
    <div className="form-field">
      <label htmlFor={name}>{label}</label>
      {component === 'textarea' ? (
        <textarea {...inputProps} />
      ) : (
        <input type="text" {...inputProps} />
      )}
    </div>
  );
});
        

Custom Hooks with Memoization

Extract memoization logic into custom hooks for reusability:


// Custom hook for form field handling
function useFormField(initialValue = '') {
  const [value, setValue] = useState(initialValue);
  
  // Memoize the change handler
  const handleChange = useCallback((e) => {
    setValue(e.target.value);
  }, []);
  
  // Memoize the reset function
  const reset = useCallback(() => {
    setValue(initialValue);
  }, [initialValue]);
  
  // Return memoized values and functions
  return {
    value,
    onChange: handleChange,
    reset
  };
}

// Usage in a component
function SimpleForm() {
  const nameField = useFormField('');
  const emailField = useFormField('');
  const messageField = useFormField('');
  
  // Memoize the submit handler
  const handleSubmit = useCallback((e) => {
    e.preventDefault();
    
    // Form submission logic
    console.log({
      name: nameField.value,
      email: emailField.value,
      message: messageField.value
    });
    
    // Reset fields
    nameField.reset();
    emailField.reset();
    messageField.reset();
  }, [nameField, emailField, messageField]);
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          id="name"
          type="text"
          {...nameField}
        />
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input
          id="email"
          type="email"
          {...emailField}
        />
      </div>
      
      <div>
        <label htmlFor="message">Message:</label>
        <textarea
          id="message"
          {...messageField}
        />
      </div>
      
      <button type="submit">Send Message</button>
    </form>
  );
}
        

Memoization with Context

Optimize context providers and consumers with memoization:


// theme-context.js
import { createContext, useContext, useMemo, useState, useCallback } from 'react';

const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  // Memoize the toggle function
  const toggleTheme = useCallback(() => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  }, []);
  
  // Memoize the context value
  const contextValue = useMemo(() => ({
    theme,
    toggleTheme
  }), [theme, toggleTheme]);
  
  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  return useContext(ThemeContext);
}

// Usage in components
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  
  // The button style is memoized based on the theme
  const buttonStyle = useMemo(() => ({
    background: theme === 'light' ? '#ffffff' : '#333333',
    color: theme === 'light' ? '#333333' : '#ffffff',
    border: `1px solid ${theme === 'light' ? '#dddddd' : '#555555'}`,
    padding: '8px 16px',
    borderRadius: '4px'
  }), [theme]);
  
  return (
    <button style={buttonStyle} onClick={toggleTheme}>
      Toggle to {theme === 'light' ? 'Dark' : 'Light'} Theme
    </button>
  );
}
        

Real-World Example: Data Grid Component

Let's see how useMemo and useCallback can be used in a complex data grid component:


function DataGrid({ data, columns, sortable = true, filterable = true }) {
  const [sortConfig, setSortConfig] = useState({
    key: null,
    direction: 'asc'
  });
  
  const [filters, setFilters] = useState({});
  const [page, setPage] = useState(1);
  const [rowsPerPage, setRowsPerPage] = useState(10);
  
  // Memoize the sorted and filtered data
  const processedData = useMemo(() => {
    console.log('Processing grid data...');
    
    // Apply filters
    let result = [...data];
    
    if (Object.keys(filters).length) {
      result = result.filter(item => {
        return Object.entries(filters).every(([key, value]) => {
          if (!value) return true;
          return String(item[key]).toLowerCase().includes(value.toLowerCase());
        });
      });
    }
    
    // Apply sorting
    if (sortConfig.key) {
      result.sort((a, b) => {
        const valueA = a[sortConfig.key];
        const valueB = b[sortConfig.key];
        
        if (valueA < valueB) {
          return sortConfig.direction === 'asc' ? -1 : 1;
        }
        if (valueA > valueB) {
          return sortConfig.direction === 'asc' ? 1 : -1;
        }
        return 0;
      });
    }
    
    return result;
  }, [data, filters, sortConfig]);
  
  // Memoize pagination calculations
  const paginationData = useMemo(() => {
    const totalItems = processedData.length;
    const totalPages = Math.ceil(totalItems / rowsPerPage);
    const startIndex = (page - 1) * rowsPerPage;
    const endIndex = startIndex + rowsPerPage;
    const currentPageData = processedData.slice(startIndex, endIndex);
    
    return {
      totalItems,
      totalPages,
      currentPageData
    };
  }, [processedData, page, rowsPerPage]);
  
  // Memoize event handlers
  const handleSort = useCallback((key) => {
    setSortConfig(prevConfig => {
      if (prevConfig.key === key) {
        return {
          key,
          direction: prevConfig.direction === 'asc' ? 'desc' : 'asc'
        };
      }
      return { key, direction: 'asc' };
    });
  }, []);
  
  const handleFilter = useCallback((key, value) => {
    setFilters(prevFilters => ({
      ...prevFilters,
      [key]: value
    }));
    // Reset to first page when filtering
    setPage(1);
  }, []);
  
  const handlePageChange = useCallback((newPage) => {
    setPage(newPage);
  }, []);
  
  const handleRowsPerPageChange = useCallback((e) => {
    setRowsPerPage(Number(e.target.value));
    setPage(1); // Reset to first page
  }, []);
  
  // Memoize header rendering
  const headerRow = useMemo(() => {
    return (
      <tr>
        {columns.map(column => (
          <th key={column.key}>
            <div className="column-header">
              <span>{column.label}</span>
              
              {sortable && (
                <button
                  className="sort-button"
                  onClick={() => handleSort(column.key)}
                >
                  {sortConfig.key === column.key ? (
                    sortConfig.direction === 'asc' ? '▲' : '▼'
                  ) : '⇵'}
                </button>
              )}
            </div>
            
            {filterable && (
              <input
                type="text"
                className="column-filter"
                placeholder={`Filter ${column.label}`}
                value={filters[column.key] || ''}
                onChange={e => handleFilter(column.key, e.target.value)}
              />
            )}
          </th>
        ))}
      </tr>
    );
  }, [columns, sortable, filterable, sortConfig, filters, handleSort, handleFilter]);
  
  return (
    <div className="data-grid-container">
      {/* Grid controls */}
      <div className="grid-controls">
        <div>
          <span>Rows per page: </span>
          <select 
            value={rowsPerPage} 
            onChange={handleRowsPerPageChange}
          >
            <option value="5">5</option>
            <option value="10">10</option>
            <option value="25">25</option>
            <option value="50">50</option>
          </select>
        </div>
        
        <div>
          <span>
            Showing {(page - 1) * rowsPerPage + 1} to {
              Math.min(page * rowsPerPage, paginationData.totalItems)
            } of {paginationData.totalItems} items
          </span>
        </div>
      </div>
      
      {/* Data table */}
      <table className="data-grid">
        <thead>
          {headerRow}
        </thead>
        <tbody>
          {paginationData.currentPageData.map(item => (
            <tr key={item.id}>
              {columns.map(column => (
                <td key={`${item.id}-${column.key}`}>
                  {column.render 
                    ? column.render(item[column.key], item) 
                    : item[column.key]}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
      
      {/* Pagination */}
      <div className="pagination">
        <button
          disabled={page === 1}
          onClick={() => handlePageChange(page - 1)}
        >
          Previous
        </button>
        
        {Array.from({ length: paginationData.totalPages }, (_, i) => i + 1)
          .filter(p => Math.abs(p - page) < 3 || p === 1 || p === paginationData.totalPages)
          .map((p, i, arr) => {
            // Add ellipsis if needed
            if (i > 0 && p > arr[i - 1] + 1) {
              return (
                <React.Fragment key={`ellipsis-${p}`}>
                  <span className="ellipsis">...</span>
                  <button
                    className={p === page ? 'active' : ''}
                    onClick={() => handlePageChange(p)}
                  >
                    {p}
                  </button>
                </React.Fragment>
              );
            }
            return (
              <button
                key={p}
                className={p === page ? 'active' : ''}
                onClick={() => handlePageChange(p)}
              >
                {p}
              </button>
            );
          })}
        
        <button
          disabled={page === paginationData.totalPages}
          onClick={() => handlePageChange(page + 1)}
        >
          Next
        </button>
      </div>
    </div>
  );
}
          

This example demonstrates a comprehensive use of memoization in a complex component:

  • Data processing is memoized to avoid expensive re-calculations
  • UI elements (header row) are memoized to avoid re-rendering
  • Event handlers are memoized to maintain stable references
  • Derived calculations (pagination) are memoized for efficiency

Without memoization, this component would perform poorly with large datasets or frequent user interactions.

Performance Testing and Measurement

To ensure your memoization efforts are effective, it's important to measure performance:

React DevTools Profiler

The React DevTools Profiler helps visualize component rendering:

  1. Record a session while interacting with your application
  2. Analyze which components are rendering and why
  3. Look for components that render unnecessarily
  4. Apply memoization strategically
  5. Record again to verify improvements

Console Logging

Basic console logging can help track renders and memoization:


// Log when the component renders
function ExpensiveComponent({ data }) {
  console.log('ExpensiveComponent rendered');
  
  // Log when calculations happen
  const processedData = useMemo(() => {
    console.log('Expensive calculation running');
    return heavyProcessing(data);
  }, [data]);
  
  // ...
}
        

Performance Hooks

Custom hooks can help track and compare performance:


// Custom hook to measure execution time
function usePerformanceMeasure(name) {
  return useCallback((callback) => {
    const start = performance.now();
    const result = callback();
    const end = performance.now();
    console.log(`${name} took ${end - start} ms`);
    return result;
  }, [name]);
}

// Usage
function DataProcessor({ data }) {
  const measure = usePerformanceMeasure('Data processing');
  
  // Measure without memoization
  const resultWithoutMemo = measure(() => {
    return processData(data);
  });
  
  // Measure with memoization
  const resultWithMemo = useMemo(() => {
    return measure(() => processData(data));
  }, [data, measure]);
  
  // Compare results
  console.log('Results match:', 
    JSON.stringify(resultWithoutMemo) === JSON.stringify(resultWithMemo)
  );
  
  // ...
}
        

When Not to Memoize

Memoization isn't always beneficial. Here are signs you might be over-optimizing:

  • Performance metrics show no significant improvement
  • Code complexity has increased substantially
  • Bugs related to stale closures are occurring
  • The application feels responsive without memoization

Remember: Premature optimization is the root of all evil. Start with clean, working code, then optimize based on measurements.

Practice Activities

Activity 1: Fixing Memoization Issues

The following code has issues with unnecessary re-renders. Use useMemo and useCallback to fix them:


function ProductFilter({ products, onFilterChange }) {
  const [filters, setFilters] = useState({
    category: '',
    minPrice: '',
    maxPrice: ''
  });
  
  // Issue 1: This function is recreated on every render
  const handleCategoryChange = (e) => {
    const newFilters = {
      ...filters,
      category: e.target.value
    };
    setFilters(newFilters);
    onFilterChange(newFilters);
  };
  
  // Issue 2: This function is recreated on every render
  const handlePriceChange = (field) => (e) => {
    const newFilters = {
      ...filters,
      [field]: e.target.value
    };
    setFilters(newFilters);
    onFilterChange(newFilters);
  };
  
  // Issue 3: This value is recalculated on every render
  const categories = Array.from(new Set(products.map(p => p.category)));
  
  // Issue 4: This object is recreated on every render
  const styles = {
    container: { padding: '1rem', border: '1px solid #ccc' },
    input: { margin: '0.5rem', padding: '0.25rem' }
  };
  
  return (
    <div style={styles.container}>
      <div>
        <label>Category: </label>
        <select 
          value={filters.category} 
          onChange={handleCategoryChange}
          style={styles.input}
        >
          <option value="">All Categories</option>
          {categories.map(category => (
            <option key={category} value={category}>
              {category}
            </option>
          ))}
        </select>
      </div>
      
      <div>
        <label>Min Price: </label>
        <input
          type="number"
          value={filters.minPrice}
          onChange={handlePriceChange('minPrice')}
          style={styles.input}
        />
      </div>
      
      <div>
        <label>Max Price: </label>
        <input
          type="number"
          value={filters.maxPrice}
          onChange={handlePriceChange('maxPrice')}
          style={styles.input}
        />
      </div>
    </div>
  );
}

// Parent component has unnecessary re-renders
function ProductPage() {
  const [products, setProducts] = useState([/* product data */]);
  const [filteredProducts, setFilteredProducts] = useState(products);
  
  // Issue 5: This function is recreated on every render
  const handleFilterChange = (filters) => {
    const result = products.filter(product => {
      if (filters.category && product.category !== filters.category) {
        return false;
      }
      
      if (filters.minPrice && product.price < Number(filters.minPrice)) {
        return false;
      }
      
      if (filters.maxPrice && product.price > Number(filters.maxPrice)) {
        return false;
      }
      
      return true;
    });
    
    setFilteredProducts(result);
  };
  
  return (
    <div>
      <h1>Products</h1>
      <ProductFilter 
        products={products} 
        onFilterChange={handleFilterChange} 
      />
      <ProductList products={filteredProducts} />
    </div>
  );
}

// Fix the issues using useMemo and useCallback
        

Activity 2: Implementing Custom Hooks with Memoization

Create a custom hook called useSearch that implements search functionality with memoization:


// Implement the useSearch hook
function useSearch(items, searchOptions = {}) {
  // The hook should:
  // 1. Accept an array of items to search
  // 2. Maintain search state (query, filters, etc.)
  // 3. Return filtered items based on search criteria
  // 4. Memoize search results and search functions
  // 5. Be efficient with dependencies
  
  // Example usage:
  // const { results, searchQuery, setSearchQuery, filteredCount } = useSearch(products, { 
  //   searchFields: ['name', 'description'], 
  //   defaultQuery: '' 
  // });
}

// Test your hook in a component
function SearchableList({ items }) {
  const { 
    results, 
    searchQuery, 
    setSearchQuery,
    filteredCount,
    totalCount
  } = useSearch(items, { 
    searchFields: ['title', 'description'] 
  });
  
  return (
    <div>
      <div className="search-bar">
        <input
          type="text"
          value={searchQuery}
          onChange={e => setSearchQuery(e.target.value)}
          placeholder="Search..."
        />
        <div className="search-stats">
          Showing {filteredCount} of {totalCount} items
        </div>
      </div>
      
      <ul className="search-results">
        {results.map(item => (
          <li key={item.id}>
            <h3>{item.title}</h3>
            <p>{item.description}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}
        

Activity 3: Analyzing and Optimizing a Complex Component

Analyze the following component for performance issues and optimize it with useMemo and useCallback:


function Dashboard() {
  const [users, setUsers] = useState([]);
  const [activities, setActivities] = useState([]);
  const [stats, setStats] = useState({});
  const [filters, setFilters] = useState({ timeRange: '7days', type: 'all' });
  const [selectedUser, setSelectedUser] = useState(null);
  
  // Fetch data
  useEffect(() => {
    Promise.all([
      fetchUsers(),
      fetchActivities(filters),
      fetchStats(filters)
    ]).then(([usersData, activitiesData, statsData]) => {
      setUsers(usersData);
      setActivities(activitiesData);
      setStats(statsData);
    });
  }, [filters]);
  
  // Process activities
  const processedActivities = activities.map(activity => {
    const user = users.find(u => u.id === activity.userId);
    return {
      ...activity,
      userName: user ? user.name : 'Unknown User',
      formattedDate: new Date(activity.timestamp).toLocaleDateString()
    };
  });
  
  // Calculate summary statistics
  const summary = {
    totalActivities: activities.length,
    averagePerDay: activities.length / 7,
    mostActiveUser: users.length > 0 ? 
      users.reduce((most, user) => {
        const userActivities = activities.filter(a => a.userId === user.id);
        return userActivities.length > most.count ? 
          { user, count: userActivities.length } : most;
      }, { user: users[0], count: 0 }).user : null,
    byType: activities.reduce((acc, activity) => {
      acc[activity.type] = (acc[activity.type] || 0) + 1;
      return acc;
    }, {})
  };
  
  // Event handlers
  const handleFilterChange = (e) => {
    const { name, value } = e.target;
    setFilters(prev => ({ ...prev, [name]: value }));
  };
  
  const handleUserSelect = (userId) => {
    setSelectedUser(userId);
  };
  
  const handleExport = () => {
    // Generate CSV export
    const csvContent = processedActivities
      .filter(a => !selectedUser || a.userId === selectedUser)
      .map(a => `${a.formattedDate},${a.userName},${a.type},${a.details}`)
      .join('\n');
    
    // Download logic
    const blob = new Blob([csvContent], { type: 'text/csv' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'activities.csv';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  };
  
  // Analyze this component and optimize it using useMemo and useCallback
  // Focus on:
  // 1. Identifying expensive calculations
  // 2. Stabilizing function references
  // 3. Preventing unnecessary re-renders
  // 4. Managing dependencies correctly
  
  return (
    <div className="dashboard">
      {/* Dashboard UI */}
    </div>
  );
}
        

Summary

Further Resources