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

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

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.