Explain React.memo, useMemo, and useCallback. What's the difference, and when should each be used?

10 minintermediatereactreact.memousememousecallbackdifference

Quick Answer

React provides three main optimization hooks/methods to prevent unnecessary re-renders and expensive calculations. Understanding when and how to use each is crucial for building performant React applications.

Detailed Answer

React provides three main optimization hooks/methods to prevent unnecessary re-renders and expensive calculations. Understanding when and how to use each is crucial for building performant React applications.

React.memo - Component Memoization

React.memo is a higher-order component that memoizes the result of a component. It only re-renders when its props change.

// Basic usage
const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
  console.log('ExpensiveComponent rendered');
  
  return (
    <div>
      <h3>Data: {data.title}</h3>
      <button onClick={() => onUpdate(data.id)}>Update</button>
    </div>
  );
});

// Custom comparison function
const UserCard = React.memo(({ user, onEdit }) => {
  return (
    <div className="user-card">
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <button onClick={() => onEdit(user.id)}>Edit</button>
    </div>
  );
}, (prevProps, nextProps) => {
  // Only re-render if specific properties change
  return (
    prevProps.user.id === nextProps.user.id &&
    prevProps.user.name === nextProps.user.name &&
    prevProps.user.email === nextProps.user.email &&
    prevProps.user.avatar === nextProps.user.avatar
  );
});

// Usage
function UserList({ users, onUserEdit }) {
  return (
    <div>
      {users.map(user => (
        <UserCard
          key={user.id}
          user={user}
          onEdit={onUserEdit}
        />
      ))}
    </div>
  );
}

useMemo - Value Memoization

useMemo memoizes the result of a computation and only recalculates when dependencies change.

function DataVisualization({ data, filters, sortBy }) {
  // Expensive calculation - only runs when data, filters, or sortBy changes
  const processedData = useMemo(() => {
    console.log('Processing data...');
    
    return data
      .filter(item => {
        if (filters.category !== 'all' && item.category !== filters.category) {
          return false;
        }
        if (filters.status !== 'all' && item.status !== filters.status) {
          return false;
        }
        return true;
      })
      .map(item => ({
        ...item,
        score: calculateComplexScore(item),
        trend: calculateTrend(item.history),
        normalizedValue: normalizeValue(item.value, data)
      }))
      .sort((a, b) => {
        switch (sortBy) {
          case 'score':
            return b.score - a.score;
          case 'name':
            return a.name.localeCompare(b.name);
          case 'date':
            return new Date(b.date) - new Date(a.date);
          default:
            return 0;
        }
      });
  }, [data, filters.category, filters.status, sortBy]);
  
  // Derived calculations - only recalculates when processedData changes
  const statistics = useMemo(() => ({
    total: processedData.length,
    average: processedData.reduce((sum, item) => sum + item.score, 0) / processedData.length,
    topPerformer: processedData[0]?.name || 'N/A',
    categories: [...new Set(processedData.map(item => item.category))],
    scoreDistribution: calculateScoreDistribution(processedData)
  }), [processedData]);
  
  // Expensive object creation - memoized to prevent child re-renders
  const chartConfig = useMemo(() => ({
    data: processedData,
    options: {
      responsive: true,
      plugins: {
        legend: { position: 'top' },
        title: { display: true, text: 'Data Visualization' }
      },
      scales: {
        y: { beginAtZero: true },
        x: { type: 'category' }
      }
    }
  }), [processedData]);
  
  return (
    <div>
      <StatisticsDisplay stats={statistics} />
      <Chart config={chartConfig} />
      <DataTable data={processedData} />
    </div>
  );
}

useCallback - Function Memoization

useCallback memoizes a function and only recreates it when dependencies change.

function UserManagement() {
  const [users, setUsers] = useState<User[]>([]);
  const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
  const [searchQuery, setSearchQuery] = useState('');
  
  // Memoize event handlers to prevent child re-renders
  const handleUserSelect = useCallback((userId: string) => {
    setSelectedUserId(userId);
  }, []);
  
  const handleUserUpdate = useCallback((userId: string, updates: Partial<User>) => {
    setUsers(prevUsers =>
      prevUsers.map(user =>
        user.id === userId ? { ...user, ...updates } : user
      )
    );
  }, []);
  
  const handleUserDelete = useCallback((userId: string) => {
    setUsers(prevUsers => prevUsers.filter(user => user.id !== userId));
    if (selectedUserId === userId) {
      setSelectedUserId(null);
    }
  }, [selectedUserId]);
  
  const handleSearch = useCallback((query: string) => {
    setSearchQuery(query);
  }, []);
  
  // Memoize filtered users
  const filteredUsers = useMemo(() => {
    if (!searchQuery) return users;
    
    return users.filter(user =>
      user.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
      user.email.toLowerCase().includes(searchQuery.toLowerCase())
    );
  }, [users, searchQuery]);
  
  // Memoize sorted users
  const sortedUsers = useMemo(() => {
    return [...filteredUsers].sort((a, b) => a.name.localeCompare(b.name));
  }, [filteredUsers]);
  
  return (
    <div>
      <SearchInput onSearch={handleSearch} />
      <UserList
        users={sortedUsers}
        onUserSelect={handleUserSelect}
        onUserUpdate={handleUserUpdate}
        onUserDelete={handleUserDelete}
      />
      {selectedUserId && (
        <UserDetails
          userId={selectedUserId}
          onUpdate={handleUserUpdate}
        />
      )}
    </div>
  );
}

Key Differences and When to Use Each:

Hook/MethodPurposeWhen to UseDependencies
React.memoPrevents component re-rendersWhen component receives same props frequentlyProps comparison
useMemoMemoizes expensive calculationsExpensive computations, object/array creationValue dependencies
useCallbackMemoizes function referencesEvent handlers, functions passed as propsFunction dependencies

Detailed Comparison:

  1. React.memo vs useMemo:
// React.memo - prevents component re-renders
const ExpensiveComponent = React.memo(({ data }) => {
  // This component only re-renders when 'data' prop changes
  return <div>{data.title}</div>;
});

// useMemo - prevents expensive calculations
function ParentComponent({ data }) {
  const expensiveValue = useMemo(() => {
    // This calculation only runs when 'data' changes
    return data.reduce((sum, item) => sum + item.value, 0);
  }, [data]);
  
  return <div>{expensiveValue}</div>;
}
  1. useCallback vs useMemo:
function Example() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  // useMemo - memoizes a value
  const expensiveValue = useMemo(() => {
    return count * 1000; // Expensive calculation
  }, [count]);
  
  // useCallback - memoizes a function
  const handleClick = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);
  
  return (
    <div>
      <p>Value: {expensiveValue}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

Advanced Patterns:

  1. Combining All Three:
// Memoized component
const UserCard = React.memo(({ user, onEdit, onDelete }) => {
  // Memoized expensive calculation
  const userStats = useMemo(() => {
    return {
      activityScore: calculateActivityScore(user),
      riskLevel: assessRiskLevel(user),
      recommendations: generateRecommendations(user)
    };
  }, [user.id, user.lastActivity, user.transactions]);
  
  // Memoized event handlers
  const handleEdit = useCallback(() => {
    onEdit(user.id);
  }, [user.id, onEdit]);
  
  const handleDelete = useCallback(() => {
    onDelete(user.id);
  }, [user.id, onDelete]);
  
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>Activity Score: {userStats.activityScore}</p>
      <p>Risk Level: {userStats.riskLevel}</p>
      <button onClick={handleEdit}>Edit</button>
      <button onClick={handleDelete}>Delete</button>
    </div>
  );
});

// Parent component with stable references
function UserList({ users, onUserEdit, onUserDelete }) {
  const handleUserEdit = useCallback((userId: string) => {
    onUserEdit(userId);
  }, [onUserEdit]);
  
  const handleUserDelete = useCallback((userId: string) => {
    onUserDelete(userId);
  }, [onUserDelete]);
  
  return (
    <div>
      {users.map(user => (
        <UserCard
          key={user.id}
          user={user}
          onEdit={handleUserEdit}
          onDelete={handleUserDelete}
        />
      ))}
    </div>
  );
}
  1. Custom Comparison Functions:
// Deep comparison for complex objects
const DeepMemoComponent = React.memo(({ config, data }) => {
  return <ComplexVisualization config={config} data={data} />;
}, (prevProps, nextProps) => {
  // Custom deep comparison
  return (
    JSON.stringify(prevProps.config) === JSON.stringify(nextProps.config) &&
    prevProps.data.length === nextProps.data.length &&
    prevProps.data.every((item, index) => 
      JSON.stringify(item) === JSON.stringify(nextProps.data[index])
    )
  );
});

// Shallow comparison for specific properties
const SelectiveMemoComponent = React.memo(({ user, settings, onUpdate }) => {
  return <UserProfile user={user} settings={settings} onUpdate={onUpdate} />;
}, (prevProps, nextProps) => {
  // Only compare specific properties
  return (
    prevProps.user.id === nextProps.user.id &&
    prevProps.user.name === nextProps.user.name &&
    prevProps.settings.theme === nextProps.settings.theme
    // Ignore other properties
  );
});
  1. Conditional Memoization:
function ConditionalComponent({ data, shouldOptimize }) {
  // Only memoize when optimization is enabled
  const processedData = shouldOptimize 
    ? useMemo(() => expensiveProcessing(data), [data])
    : expensiveProcessing(data);
  
  return <div>{processedData}</div>;
}

// Or with custom hook
function useConditionalMemo<T>(factory: () => T, deps: any[], condition: boolean): T {
  return condition ? useMemo(factory, deps) : factory();
}

Common Pitfalls and Best Practices:

  1. Don't Over-Memoize:
// BAD - Unnecessary memoization
function SimpleComponent({ name }) {
  const memoizedName = useMemo(() => name, [name]); // Unnecessary!
  return <div>{memoizedName}</div>;
}

// GOOD - Only memoize when needed
function SimpleComponent({ name }) {
  return <div>{name}</div>; // Simple values don't need memoization
}
  1. Include All Dependencies:
// BAD - Missing dependencies
function BadExample({ userId, filters }) {
  const data = useMemo(() => {
    return fetchData(userId, filters); // filters not in deps!
  }, [userId]);
  
  return <div>{data}</div>;
}

// GOOD - Include all dependencies
function GoodExample({ userId, filters }) {
  const data = useMemo(() => {
    return fetchData(userId, filters);
  }, [userId, filters]);
  
  return <div>{data}</div>;
}
  1. Use Stable References:
// BAD - New object on every render
function BadParent() {
  const config = { theme: 'dark', size: 'large' }; // New object every time!
  
  return <ChildComponent config={config} />;
}

// GOOD - Memoized object
function GoodParent() {
  const config = useMemo(() => ({
    theme: 'dark',
    size: 'large'
  }), []);
  
  return <ChildComponent config={config} />;
}

Coding Challenge: Identify and fix performance issues in a provided code sample with excessive re-renders.

Problem Code:

function UserDashboard({ userId }) {
  const [users, setUsers] = useState([]);
  const [selectedUser, setSelectedUser] = useState(null);
  const [filters, setFilters] = useState({ status: 'all', role: 'all' });
  
  // Problem 1: Expensive calculation runs on every render
  const processedUsers = users.map(user => ({
    ...user,
    score: calculateUserScore(user),
    risk: assessRisk(user),
    recommendations: generateRecommendations(user)
  }));
  
  // Problem 2: New object created on every render
  const chartData = {
    users: processedUsers,
    config: { type: 'bar', colors: ['blue', 'green'] }
  };
  
  // Problem 3: New function created on every render
  const handleUserSelect = (user) => {
    setSelectedUser(user);
  };
  
  // Problem 4: New function created on every render
  const handleFilterChange = (newFilters) => {
    setFilters(newFilters);
  };
  
  // Problem 5: Expensive filtering runs on every render
  const filteredUsers = processedUsers.filter(user => {
    if (filters.status !== 'all' && user.status !== filters.status) return false;
    if (filters.role !== 'all' && user.role !== filters.role) return false;
    return true;
  });
  
  return (
    <div>
      <FilterPanel filters={filters} onFilterChange={handleFilterChange} />
      <UserChart data={chartData} />
      <UserList 
        users={filteredUsers} 
        onUserSelect={handleUserSelect}
      />
      {selectedUser && <UserDetails user={selectedUser} />}
    </div>
  );
}

Optimized Solution:

function UserDashboard({ userId }) {
  const [users, setUsers] = useState([]);
  const [selectedUser, setSelectedUser] = useState(null);
  const [filters, setFilters] = useState({ status: 'all', role: 'all' });
  
  // Fix 1: Memoize expensive calculations
  const processedUsers = useMemo(() => {
    return users.map(user => ({
      ...user,
      score: calculateUserScore(user),
      risk: assessRisk(user),
      recommendations: generateRecommendations(user)
    }));
  }, [users]);
  
  // Fix 2: Memoize filtered users
  const filteredUsers = useMemo(() => {
    return processedUsers.filter(user => {
      if (filters.status !== 'all' && user.status !== filters.status) return false;
      if (filters.role !== 'all' && user.role !== filters.role) return false;
      return true;
    });
  }, [processedUsers, filters.status, filters.role]);
  
  // Fix 3: Memoize chart data object
  const chartData = useMemo(() => ({
    users: filteredUsers,
    config: { type: 'bar', colors: ['blue', 'green'] }
  }), [filteredUsers]);
  
  // Fix 4: Memoize event handlers
  const handleUserSelect = useCallback((user) => {
    setSelectedUser(user);
  }, []);
  
  const handleFilterChange = useCallback((newFilters) => {
    setFilters(newFilters);
  }, []);
  
  return (
    <div>
      <FilterPanel filters={filters} onFilterChange={handleFilterChange} />
      <UserChart data={chartData} />
      <UserList 
        users={filteredUsers} 
        onUserSelect={handleUserSelect}
      />
      {selectedUser && <UserDetails user={selectedUser} />}
    </div>
  );
}

// Fix 5: Memoize child components
const UserList = React.memo(({ users, onUserSelect }) => {
  return (
    <div>
      {users.map(user => (
        <UserCard
          key={user.id}
          user={user}
          onSelect={onUserSelect}
        />
      ))}
    </div>
  );
});

const UserCard = React.memo(({ user, onSelect }) => {
  const handleClick = useCallback(() => {
    onSelect(user);
  }, [user, onSelect]);
  
  return (
    <div onClick={handleClick}>
      <h3>{user.name}</h3>
      <p>Score: {user.score}</p>
      <p>Risk: {user.risk}</p>
    </div>
  );
});

Performance Monitoring:

// Custom hook to measure render performance
function useRenderPerformance(componentName: string) {
  const renderCount = useRef(0);
  const startTime = useRef(performance.now());
  
  renderCount.current++;
  
  useEffect(() => {
    const endTime = performance.now();
    const renderTime = endTime - startTime.current;
    
    console.log(`${componentName} rendered ${renderCount.current} times in ${renderTime.toFixed(2)}ms`);
    
    startTime.current = performance.now();
  });
}

// Usage
function OptimizedComponent() {
  useRenderPerformance('OptimizedComponent');
  
  return <div>Component content</div>;
}

Key Takeaways:

  1. React.memo: Use for components that receive the same props frequently
  2. useMemo: Use for expensive calculations and object/array creation
  3. useCallback: Use for functions passed as props to prevent child re-renders
  4. Measure First: Use React DevTools Profiler to identify actual bottlenecks
  5. Don't Over-Optimize: Only optimize when you have performance issues
  6. Include Dependencies: Always include all dependencies in dependency arrays
  7. Stable References: Ensure objects and functions have stable references

The key is to understand that these optimizations come with a cost (memory usage, complexity) and should only be used when they provide actual performance benefits.