Performance Optimization with React.memo and useMemo

Making Your React Applications Lightning Fast

Welcome to Performance Optimization!

Imagine you're a chef in a busy restaurant. You don't want to remake the same dish from scratch every time it's ordered - you prepare some components in advance and reuse them. Similarly, React.memo and useMemo help us avoid unnecessary work by remembering previous results. Today, we'll master the art of optimizing React applications!

graph TD A[Component Render] --> B{Props Changed?} B -->|Yes| C[Re-render Component] B -->|No| D[Use Memoized Version] C --> E[Execute Expensive Calculations] D --> F[Skip Calculations] E --> G[Update UI] F --> G style A fill:#ff9999 style D fill:#99ff99 style F fill:#99ff99

Understanding React.memo

1. Basic React.memo Usage

// Without React.memo - re-renders on every parent update
function ExpensiveComponent({ data }) {
  console.log('ExpensiveComponent rendered');
  
  // Simulate expensive operation
  const processedData = data.map(item => {
    // Complex calculations
    return item * 2;
  });
  
  return (
    <div>
      <h3>Processed Data</h3>
      {processedData.map((item, index) => (
        <div key={index}>{item}</div>
      ))}
    </div>
  );
}

// With React.memo - only re-renders when props change
const MemoizedComponent = React.memo(function ExpensiveComponent({ data }) {
  console.log('MemoizedComponent rendered');
  
  const processedData = data.map(item => item * 2);
  
  return (
    <div>
      <h3>Processed Data</h3>
      {processedData.map((item, index) => (
        <div key={index}>{item}</div>
      ))}
    </div>
  );
});

// Parent component
function Parent() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState([1, 2, 3, 4, 5]);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Increment Count: {count}
      </button>
      
      {/* This will re-render on every count change */}
      <ExpensiveComponent data={data} />
      
      {/* This will only re-render when data changes */}
      <MemoizedComponent data={data} />
    </div>
  );
}

2. Custom Comparison Function

// React.memo with custom comparison
const MemoizedComponent = React.memo(
  function UserCard({ user }) {
    console.log('UserCard rendered for:', user.name);
    
    return (
      <div className="user-card">
        <h3>{user.name}</h3>
        <p>Email: {user.email}</p>
        <p>Last Active: {user.lastActive}</p>
      </div>
    );
  },
  // Custom comparison function
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    // Return false if props are different (re-render)
    
    // Only re-render if name or email changes
    // Ignore lastActive changes
    return (
      prevProps.user.name === nextProps.user.name &&
      prevProps.user.email === nextProps.user.email
    );
  }
);

// Advanced comparison for complex objects
const ComplexMemoizedComponent = React.memo(
  function DataGrid({ data, config, onItemClick }) {
    return (
      <div className="data-grid">
        {data.map(item => (
          <div 
            key={item.id} 
            onClick={() => onItemClick(item)}
            className={`grid-item ${config.highlight === item.id ? 'highlighted' : ''}`}
          >
            {item.name}
          </div>
        ))}
      </div>
    );
  },
  (prevProps, nextProps) => {
    // Deep comparison for data array
    const dataEqual = JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data);
    
    // Shallow comparison for config
    const configEqual = prevProps.config.highlight === nextProps.config.highlight;
    
    // Reference comparison for callback
    const callbackEqual = prevProps.onItemClick === nextProps.onItemClick;
    
    return dataEqual && configEqual && callbackEqual;
  }
);

// Helper function for deep comparison
function arePropsEqual(prevProps, nextProps) {
  // Custom deep equality check
  return Object.keys(prevProps).every(key => {
    if (typeof prevProps[key] === 'function') {
      // Functions must be the same reference
      return prevProps[key] === nextProps[key];
    }
    if (typeof prevProps[key] === 'object') {
      // Deep compare objects
      return JSON.stringify(prevProps[key]) === JSON.stringify(nextProps[key]);
    }
    // Primitive values
    return prevProps[key] === nextProps[key];
  });
}

const SmartMemoizedComponent = React.memo(MyComponent, arePropsEqual);

3. When to Use React.memo

// Good use case: Expensive rendering component
const ExpensiveChart = React.memo(function Chart({ data, options }) {
  // Complex chart rendering logic
  const chartInstance = useRef(null);
  
  useEffect(() => {
    if (!chartInstance.current) {
      chartInstance.current = new ChartLibrary(canvasRef.current);
    }
    chartInstance.current.update(data, options);
  }, [data, options]);
  
  return <canvas ref={canvasRef} />;
});

// Good use case: Frequently re-rendered parent
function Dashboard() {
  const [activeTab, setActiveTab] = useState('overview');
  const [notifications, setNotifications] = useState([]);
  
  // This component re-renders often due to notifications
  useEffect(() => {
    const ws = new WebSocket('ws://api/notifications');
    ws.onmessage = (event) => {
      setNotifications(prev => [...prev, event.data]);
    };
    return () => ws.close();
  }, []);
  
  return (
    <div>
      <TabBar activeTab={activeTab} onTabChange={setActiveTab} />
      
      {/* Memoize children to prevent unnecessary re-renders */}
      <MemoizedSidebar />
      <MemoizedMainContent activeTab={activeTab} />
      
      {/* This updates frequently but shouldn't affect other components */}
      <NotificationBadge count={notifications.length} />
    </div>
  );
}

// Bad use case: Simple component with few props
// Don't over-optimize!
const SimpleButton = React.memo(function Button({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
}); // Probably unnecessary - the overhead might be more than the benefit

Understanding useMemo

1. Basic useMemo Usage

// Without useMemo - recalculates on every render
function FilteredList({ items, filter }) {
  console.log('Filtering items...');
  
  // This runs on every render, even if items and filter haven't changed
  const filteredItems = items.filter(item => 
    item.name.toLowerCase().includes(filter.toLowerCase())
  );
  
  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

// With useMemo - only recalculates when dependencies change
function OptimizedFilteredList({ items, filter }) {
  // Only recalculate when items or filter changes
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);
  
  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

2. Complex Calculations with useMemo

function DataAnalytics({ data, config }) {
  // Expensive calculation #1: Statistical analysis
  const statistics = useMemo(() => {
    console.log('Calculating statistics...');
    
    return {
      mean: data.reduce((sum, val) => sum + val, 0) / data.length,
      median: calculateMedian(data),
      mode: calculateMode(data),
      standardDeviation: calculateStandardDeviation(data),
      percentiles: calculatePercentiles(data, [25, 50, 75, 90, 95, 99])
    };
  }, [data]);
  
  // Expensive calculation #2: Data transformation
  const transformedData = useMemo(() => {
    console.log('Transforming data...');
    
    return data.map(value => ({
      original: value,
      normalized: (value - statistics.mean) / statistics.standardDeviation,
      percentile: calculatePercentileRank(value, data),
      category: categorizeValue(value, config.thresholds)
    }));
  }, [data, statistics, config.thresholds]);
  
  // Expensive calculation #3: Visualization data
  const chartData = useMemo(() => {
    console.log('Preparing chart data...');
    
    const bins = createHistogramBins(data, config.binCount);
    const distribution = calculateDistribution(data, bins);
    
    return {
      histogram: bins.map((bin, i) => ({
        x: bin.start,
        y: distribution[i]
      })),
      boxPlot: {
        min: Math.min(...data),
        q1: statistics.percentiles[0],
        median: statistics.median,
        q3: statistics.percentiles[2],
        max: Math.max(...data)
      },
      densityCurve: calculateKernelDensity(data, config.bandwidth)
    };
  }, [data, config.binCount, config.bandwidth, statistics]);
  
  return (
    <div className="analytics-dashboard">
      <StatisticsSummary stats={statistics} />
      <DataTable data={transformedData} />
      <VisualizationPanel chartData={chartData} />
    </div>
  );
}

3. Memoizing Object and Array Values

function SearchComponent({ defaultFilters }) {
  const [searchTerm, setSearchTerm] = useState('');
  const [sortOrder, setSortOrder] = useState('asc');
  
  // Without useMemo: creates new object on every render
  // This causes child components to re-render unnecessarily
  const badFilterConfig = {
    term: searchTerm,
    order: sortOrder,
    ...defaultFilters
  };
  
  // With useMemo: only creates new object when dependencies change
  const filterConfig = useMemo(() => ({
    term: searchTerm,
    order: sortOrder,
    ...defaultFilters
  }), [searchTerm, sortOrder, defaultFilters]);
  
  // Memoizing array transformations
  const sortOptions = useMemo(() => [
    { value: 'asc', label: 'Ascending' },
    { value: 'desc', label: 'Descending' },
    { value: 'alpha', label: 'Alphabetical' },
    { value: 'date', label: 'By Date' }
  ], []); // Empty deps array - this never changes
  
  // Memoizing callback creation
  const handleSearch = useMemo(() => {
    return debounce((term) => {
      // API call logic here
      console.log('Searching for:', term);
    }, 300);
  }, []); // Create debounced function only once
  
  return (
    <div>
      <SearchInput 
        value={searchTerm}
        onChange={setSearchTerm}
        onSearch={handleSearch}
      />
      <SortSelector 
        options={sortOptions}
        value={sortOrder}
        onChange={setSortOrder}
      />
      <FilteredResults config={filterConfig} />
    </div>
  );
}

Combining React.memo and useMemo

// Parent component with expensive calculations
function DataDashboard({ rawData, filters }) {
  const [sortConfig, setSortConfig] = useState({ key: 'name', order: 'asc' });
  const [selectedItems, setSelectedItems] = useState(new Set());
  
  // Expensive data processing
  const processedData = useMemo(() => {
    console.log('Processing data...');
    return rawData
      .filter(item => applyFilters(item, filters))
      .map(item => enrichData(item))
      .sort((a, b) => sortData(a, b, sortConfig));
  }, [rawData, filters, sortConfig]);
  
  // Expensive statistics calculation
  const statistics = useMemo(() => {
    console.log('Calculating statistics...');
    return {
      total: processedData.length,
      averageValue: calculateAverage(processedData),
      distribution: calculateDistribution(processedData)
    };
  }, [processedData]);
  
  // Memoize callbacks to prevent child re-renders
  const handleSort = useCallback((key) => {
    setSortConfig(prev => ({
      key,
      order: prev.key === key && prev.order === 'asc' ? 'desc' : 'asc'
    }));
  }, []);
  
  const handleSelect = useCallback((itemId) => {
    setSelectedItems(prev => {
      const next = new Set(prev);
      if (next.has(itemId)) {
        next.delete(itemId);
      } else {
        next.add(itemId);
      }
      return next;
    });
  }, []);
  
  return (
    <div>
      <Statistics data={statistics} />
      <DataTable 
        data={processedData}
        onSort={handleSort}
        sortConfig={sortConfig}
        selectedItems={selectedItems}
        onSelect={handleSelect}
      />
    </div>
  );
}

// Child component optimized with React.memo
const Statistics = React.memo(function Statistics({ data }) {
  console.log('Statistics rendered');
  
  return (
    <div className="statistics">
      <div>Total Items: {data.total}</div>
      <div>Average: {data.averageValue.toFixed(2)}</div>
      <DistributionChart data={data.distribution} />
    </div>
  );
});

// Another child component with internal memoization
const DataTable = React.memo(function DataTable({ 
  data, 
  onSort, 
  sortConfig, 
  selectedItems,
  onSelect 
}) {
  console.log('DataTable rendered');
  
  // Memoize column definitions
  const columns = useMemo(() => [
    { 
      key: 'select',
      header: '',
      render: (item) => (
        <input
          type="checkbox"
          checked={selectedItems.has(item.id)}
          onChange={() => onSelect(item.id)}
        />
      )
    },
    {
      key: 'name',
      header: 'Name',
      sortable: true
    },
    {
      key: 'value',
      header: 'Value',
      sortable: true,
      render: (item) => item.value.toFixed(2)
    }
  ], [selectedItems, onSelect]);
  
  // Memoize header rendering
  const headers = useMemo(() => (
    <thead>
      <tr>
        {columns.map(column => (
          <th 
            key={column.key}
            onClick={() => column.sortable && onSort(column.key)}
            className={sortConfig.key === column.key ? sortConfig.order : ''}
          >
            {column.header}
            {column.sortable && sortConfig.key === column.key && (
              <span>{sortConfig.order === 'asc' ? '↑' : '↓'}</span>
            )}
          </th>
        ))}
      </tr>
    </thead>
  ), [columns, sortConfig, onSort]);
  
  return (
    <table>
      {headers}
      <tbody>
        {data.map(item => (
          <tr key={item.id}>
            {columns.map(column => (
              <td key={column.key}>
                {column.render ? column.render(item) : item[column.key]}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
});

Common Performance Patterns

1. Memoizing Expensive Filters

function ProductList({ products, filters }) {
  // Memoize filtered products
  const filteredProducts = useMemo(() => {
    return products.filter(product => {
      // Price filter
      if (filters.minPrice && product.price < filters.minPrice) return false;
      if (filters.maxPrice && product.price > filters.maxPrice) return false;
      
      // Category filter
      if (filters.category && product.category !== filters.category) return false;
      
      // Search filter
      if (filters.search) {
        const searchLower = filters.search.toLowerCase();
        const inName = product.name.toLowerCase().includes(searchLower);
        const inDescription = product.description.toLowerCase().includes(searchLower);
        if (!inName && !inDescription) return false;
      }
      
      // Rating filter
      if (filters.minRating && product.rating < filters.minRating) return false;
      
      return true;
    });
  }, [products, filters]);
  
  // Memoize sorted products
  const sortedProducts = useMemo(() => {
    const sorted = [...filteredProducts];
    
    switch (filters.sortBy) {
      case 'price-low':
        return sorted.sort((a, b) => a.price - b.price);
      case 'price-high':
        return sorted.sort((a, b) => b.price - a.price);
      case 'rating':
        return sorted.sort((a, b) => b.rating - a.rating);
      case 'name':
        return sorted.sort((a, b) => a.name.localeCompare(b.name));
      default:
        return sorted;
    }
  }, [filteredProducts, filters.sortBy]);
  
  // Memoize pagination
  const paginatedProducts = useMemo(() => {
    const start = (filters.page - 1) * filters.perPage;
    const end = start + filters.perPage;
    return sortedProducts.slice(start, end);
  }, [sortedProducts, filters.page, filters.perPage]);
  
  return (
    <div>
      <div>Showing {filteredProducts.length} products</div>
      <ProductGrid products={paginatedProducts} />
      <Pagination 
        total={filteredProducts.length}
        perPage={filters.perPage}
        currentPage={filters.page}
      />
    </div>
  );
}

2. Memoizing Context Values

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [accentColor, setAccentColor] = useState('#007bff');
  
  // Without memoization: creates new object every render
  // This causes all consumers to re-render
  const badContextValue = {
    theme,
    accentColor,
    setTheme,
    setAccentColor,
    toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light')
  };
  
  // With memoization: only updates when dependencies change
  const contextValue = useMemo(() => ({
    theme,
    accentColor,
    setTheme,
    setAccentColor,
    toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light')
  }), [theme, accentColor]);
  
  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
}

// Advanced: Split context for better performance
function AppProviders({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [language, setLanguage] = useState('en');
  
  // Separate contexts for different concerns
  const authValue = useMemo(() => ({ user, setUser }), [user]);
  const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
  const i18nValue = useMemo(() => ({ language, setLanguage }), [language]);
  
  return (
    <AuthContext.Provider value={authValue}>
      <ThemeContext.Provider value={themeValue}>
        <I18nContext.Provider value={i18nValue}>
          {children}
        </I18nContext.Provider>
      </ThemeContext.Provider>
    </AuthContext.Provider>
  );
}

3. Memoizing Heavy Computations

function ChartComponent({ data, config }) {
  // Expensive data processing for visualization
  const chartData = useMemo(() => {
    console.log('Processing chart data...');
    
    // Step 1: Clean and validate data
    const cleanedData = data.filter(item => 
      item.value !== null && !isNaN(item.value)
    );
    
    // Step 2: Calculate aggregates
    const aggregates = cleanedData.reduce((acc, item) => {
      const key = item[config.groupBy];
      if (!acc[key]) {
        acc[key] = { sum: 0, count: 0, values: [] };
      }
      acc[key].sum += item.value;
      acc[key].count += 1;
      acc[key].values.push(item.value);
      return acc;
    }, {});
    
    // Step 3: Calculate statistics for each group
    const statistics = Object.entries(aggregates).map(([key, group]) => ({
      key,
      average: group.sum / group.count,
      median: calculateMedian(group.values),
      min: Math.min(...group.values),
      max: Math.max(...group.values),
      count: group.count
    }));
    
    // Step 4: Format for chart library
    return {
      labels: statistics.map(stat => stat.key),
      datasets: [{
        label: 'Average',
        data: statistics.map(stat => stat.average)
      }, {
        label: 'Median',
        data: statistics.map(stat => stat.median)
      }],
      metadata: statistics
    };
  }, [data, config.groupBy]);
  
  // Memoize chart options
  const chartOptions = useMemo(() => ({
    responsive: true,
    plugins: {
      legend: {
        position: config.legendPosition || 'top',
      },
      title: {
        display: true,
        text: config.title || 'Data Visualization'
      }
    },
    scales: {
      y: {
        beginAtZero: config.beginAtZero !== false
      }
    }
  }), [config]);
  
  return (
    <div className="chart-container">
      <Chart 
        type={config.chartType || 'bar'}
        data={chartData}
        options={chartOptions}
      />
      <DataSummary statistics={chartData.metadata} />
    </div>
  );
}

Common Pitfalls and Solutions

1. Incorrect Dependencies

// WRONG: Missing dependency
function BadExample({ items }) {
  const [filter, setFilter] = useState('');
  
  // Missing 'filter' in dependency array!
  const filtered = useMemo(() => {
    return items.filter(item => item.includes(filter));
  }, [items]); // Should be [items, filter]
  
  return <List items={filtered} />;
}

// CORRECT: All dependencies included
function GoodExample({ items }) {
  const [filter, setFilter] = useState('');
  
  const filtered = useMemo(() => {
    return items.filter(item => item.includes(filter));
  }, [items, filter]);
  
  return <List items={filtered} />;
}

// Using ESLint to catch dependency issues
// Install eslint-plugin-react-hooks
// Add to .eslintrc:
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/exhaustive-deps": "warn"
  }
}

2. Over-optimization

// OVER-OPTIMIZED: Simple calculations don't need memoization
function OverOptimized({ count }) {
  // This is unnecessary - the calculation is trivial
  const doubled = useMemo(() => count * 2, [count]);
  
  // This is also unnecessary for a simple component
  const button = useMemo(() => (
    <button>Click me</button>
  ), []);
  
  return <div>{doubled} {button}</div>;
}

// BETTER: Only optimize what's necessary
function Balanced({ items, complexFilter }) {
  // Only memoize expensive operations
  const filtered = useMemo(() => {
    return items.filter(item => complexFilter.test(item));
  }, [items, complexFilter]);
  
  // Simple calculations can be inline
  const count = filtered.length;
  const message = `Found ${count} items`;
  
  return <div>{message}</div>;
}

3. Reference Equality Issues

// PROBLEM: Objects/arrays break memoization
function Parent() {
  const [count, setCount] = useState(0);
  
  // This creates a new object on every render!
  const config = { color: 'blue', size: 'large' };
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      {/* Child will re-render every time! */}
      <MemoizedChild config={config} />
    </div>
  );
}

// SOLUTION: Memoize object creation
function ParentFixed() {
  const [count, setCount] = useState(0);
  
  // Only create object once
  const config = useMemo(() => ({
    color: 'blue',
    size: 'large'
  }), []);
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <MemoizedChild config={config} />
    </div>
  );
}

// Also applies to callbacks
function ParentWithCallback() {
  const [count, setCount] = useState(0);
  
  // This creates a new function on every render!
  const handleClick = () => {
    console.log('clicked');
  };
  
  // Use useCallback instead
  const handleClickMemoized = useCallback(() => {
    console.log('clicked');
  }, []);
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <MemoizedChild onClick={handleClickMemoized} />
    </div>
  );
}

Performance Debugging

1. Using React DevTools Profiler

// Add profiler to measure performance
import { Profiler } from 'react';

function onRenderCallback(
  id, // the "id" prop of the Profiler tree that has just committed
  phase, // either "mount" (if the tree just mounted) or "update"
  actualDuration, // time spent rendering the committed update
  baseDuration, // estimated time to render the entire subtree without memoization
  startTime, // when React began rendering this update
  commitTime, // when React committed this update
  interactions // the Set of interactions belonging to this update
) {
  console.log(`${id} (${phase}) took ${actualDuration}ms`);
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <MainContent />
    </Profiler>
  );
}

2. Custom Performance Hooks

// Hook to measure render count
function useRenderCount(componentName) {
  const renderCount = useRef(0);
  renderCount.current += 1;
  
  useEffect(() => {
    console.log(`${componentName} rendered ${renderCount.current} times`);
  });
}

// Hook to measure computation time
function useComputationTime(label) {
  const startTime = useRef(performance.now());
  
  useEffect(() => {
    const endTime = performance.now();
    const duration = endTime - startTime.current;
    console.log(`${label} took ${duration.toFixed(2)}ms`);
    startTime.current = performance.now();
  });
}

// Hook to detect unnecessary re-renders
function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();
  
  useEffect(() => {
    if (previousProps.current) {
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      const changedProps = {};
      
      allKeys.forEach(key => {
        if (previousProps.current[key] !== props[key]) {
          changedProps[key] = {
            from: previousProps.current[key],
            to: props[key]
          };
        }
      });
      
      if (Object.keys(changedProps).length) {
        console.log(`[why-did-you-update] ${name}:`, changedProps);
      }
    }
    
    previousProps.current = props;
  });
}

Best Practices

1. Measure Before Optimizing

// Use React DevTools Profiler first
// Only optimize components that actually need it
function MaybeOptimize({ data }) {
  // First, check if this component is a performance bottleneck
  // Use React DevTools to measure render time
  
  // If it's slow, then optimize:
  const processedData = useMemo(() => {
    return expensiveOperation(data);
  }, [data]);
  
  return <DisplayData data={processedData} />;
}

2. Start with React.memo

// Often React.memo is enough
const ExpensiveComponent = React.memo(function({ data }) {
  // Component logic here
});

// Add useMemo only if needed
const OptimizedComponent = React.memo(function({ data }) {
  const processed = useMemo(() => {
    return expensiveCalculation(data);
  }, [data]);
  
  return <Display data={processed} />;
});

3. Keep Dependencies Stable

// Move static values outside component
const STATIC_CONFIG = {
  threshold: 100,
  maxItems: 50
};

function Component({ items }) {
  // This is stable across renders
  const filtered = useMemo(() => {
    return items.filter(item => item.value > STATIC_CONFIG.threshold);
  }, [items]); // STATIC_CONFIG doesn't need to be a dependency
  
  return <List items={filtered} />;
}

Practice Exercises

Exercise 1: Optimize a Data Table

Create a data table component that:

  • Displays 1000+ rows of data
  • Supports sorting by multiple columns
  • Has filtering capabilities
  • Uses virtualization for rendering
  • Implements proper memoization

Exercise 2: Build a Chart Dashboard

Create a dashboard with multiple charts:

  • Multiple chart types (bar, line, pie)
  • Real-time data updates
  • Expensive data processing
  • Proper memoization of calculations
  • Optimize re-renders when data changes

Exercise 3: Optimize a Form

Create a complex form with:

  • Multiple sections that can be expanded/collapsed
  • Dynamic field validation
  • Conditional fields based on other values
  • Autosave functionality
  • Prevent unnecessary re-renders

Key Takeaways