# Performance Optimization Patterns for optimizing React component performance, preventing unnecessary re-renders, and avoiding memory leaks. --- ## Memoization Patterns ### useMemo for Expensive Computations ```typescript import { useMemo } from 'react'; export const DataDisplay: React.FC<{ items: Item[], searchTerm: string }> = ({ items, searchTerm, }) => { // ❌ AVOID - Runs on every render const filteredItems = items .filter(item => item.name.includes(searchTerm)) .sort((a, b) => a.name.localeCompare(b.name)); // ✅ CORRECT - Memoized, only recalculates when dependencies change const filteredItems = useMemo(() => { return items .filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase())) .sort((a, b) => a.name.localeCompare(b.name)); }, [items, searchTerm]); return ; }; ``` **When to use useMemo:** - Filtering/sorting large arrays - Complex calculations - Transforming data structures - Expensive computations (loops, recursion) **When NOT to use useMemo:** - Simple string concatenation - Basic arithmetic - Premature optimization (profile first!) --- ## useCallback for Event Handlers ### The Problem ```typescript // ❌ AVOID - Creates new function on every render export const Parent: React.FC = () => { const handleClick = (id: string) => { console.log('Clicked:', id); }; // Child re-renders every time Parent renders // because handleClick is a new function reference each time return ; }; ``` ### The Solution ```typescript import { useCallback } from 'react'; export const Parent: React.FC = () => { // ✅ CORRECT - Stable function reference const handleClick = useCallback((id: string) => { console.log('Clicked:', id); }, []); // Empty deps = function never changes // Child only re-renders when props actually change return ; }; ``` **When to use useCallback:** - Functions passed as props to children - Functions used as dependencies in useEffect - Functions passed to memoized components - Event handlers in lists **When NOT to use useCallback:** - Event handlers not passed to children - Simple inline handlers: `onClick={() => doSomething()}` --- ## React.memo for Component Memoization ### Basic Usage ```typescript import React from 'react'; interface ExpensiveComponentProps { data: ComplexData; onAction: () => void; } // ✅ Wrap expensive components in React.memo export const ExpensiveComponent = React.memo( function ExpensiveComponent({ data, onAction }) { // Complex rendering logic return ; } ); ``` **When to use React.memo:** - Component renders frequently - Component has expensive rendering - Props don't change often - Component is a list item - DataGrid cells/renderers **When NOT to use React.memo:** - Props change frequently anyway - Rendering is already fast - Premature optimization --- ## Debounced Search ### Using use-debounce Hook ```typescript import { useState } from 'react'; import { useDebounce } from 'use-debounce'; import { useSuspenseQuery } from '@tanstack/react-query'; export const SearchComponent: React.FC = () => { const [searchTerm, setSearchTerm] = useState(''); // Debounce for 300ms const [debouncedSearchTerm] = useDebounce(searchTerm, 300); // Query uses debounced value const { data } = useSuspenseQuery({ queryKey: ['search', debouncedSearchTerm], queryFn: () => api.search(debouncedSearchTerm), enabled: debouncedSearchTerm.length > 0, }); return ( setSearchTerm(e.target.value)} placeholder='Search...' /> ); }; ``` **Optimal Debounce Timing:** - **300-500ms**: Search/filtering - **1000ms**: Auto-save - **100-200ms**: Real-time validation --- ## Memory Leak Prevention ### Cleanup Timeouts/Intervals ```typescript import { useEffect, useState } from 'react'; export const MyComponent: React.FC = () => { const [count, setCount] = useState(0); useEffect(() => { // ✅ CORRECT - Cleanup interval const intervalId = setInterval(() => { setCount(c => c + 1); }, 1000); return () => { clearInterval(intervalId); // Cleanup! }; }, []); useEffect(() => { // ✅ CORRECT - Cleanup timeout const timeoutId = setTimeout(() => { console.log('Delayed action'); }, 5000); return () => { clearTimeout(timeoutId); // Cleanup! }; }, []); return
{count}
; }; ``` ### Cleanup Event Listeners ```typescript useEffect(() => { const handleResize = () => { console.log('Resized'); }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); // Cleanup! }; }, []); ``` ### Abort Controllers for Fetch ```typescript useEffect(() => { const abortController = new AbortController(); fetch('/api/data', { signal: abortController.signal }) .then(response => response.json()) .then(data => setState(data)) .catch(error => { if (error.name === 'AbortError') { console.log('Fetch aborted'); } }); return () => { abortController.abort(); // Cleanup! }; }, []); ``` **Note**: With TanStack Query, this is handled automatically. --- ## Form Performance ### Watch Specific Fields (Not All) ```typescript import { useForm } from 'react-hook-form'; export const MyForm: React.FC = () => { const { register, watch, handleSubmit } = useForm(); // ❌ AVOID - Watches all fields, re-renders on any change const formValues = watch(); // ✅ CORRECT - Watch only what you need const username = watch('username'); const email = watch('email'); // Or multiple specific fields const [username, email] = watch(['username', 'email']); return (
{/* Only re-renders when username/email change */}

Username: {username}, Email: {email}

); }; ``` --- ## List Rendering Optimization ### Key Prop Usage ```typescript // ✅ CORRECT - Stable unique keys {items.map(item => ( {item.name} ))} // ❌ AVOID - Index as key (unstable if list changes) {items.map((item, index) => ( // WRONG if list reorders {item.name} ))} ``` ### Memoized List Items ```typescript const ListItem = React.memo(({ item, onAction }) => { return ( onAction(item.id)}> {item.name} ); }); export const List: React.FC<{ items: Item[] }> = ({ items }) => { const handleAction = useCallback((id: string) => { console.log('Action:', id); }, []); return ( {items.map(item => ( ))} ); }; ``` --- ## Preventing Component Re-initialization ### The Problem ```typescript // ❌ AVOID - Component recreated on every render export const Parent: React.FC = () => { // New component definition each render! const ChildComponent = () =>
Child
; return ; // Unmounts and remounts every render }; ``` ### The Solution ```typescript // ✅ CORRECT - Define outside or use useMemo const ChildComponent: React.FC = () =>
Child
; export const Parent: React.FC = () => { return ; // Stable component }; // ✅ OR if dynamic, use useMemo export const Parent: React.FC<{ config: Config }> = ({ config }) => { const DynamicComponent = useMemo(() => { return () =>
{config.title}
; }, [config.title]); return ; }; ``` --- ## Lazy Loading Heavy Dependencies ### Code Splitting ```typescript // ❌ AVOID - Import heavy libraries at top level import jsPDF from 'jspdf'; // Large library loaded immediately import * as XLSX from 'xlsx'; // Large library loaded immediately // ✅ CORRECT - Dynamic import when needed const handleExportPDF = async () => { const { jsPDF } = await import('jspdf'); const doc = new jsPDF(); // Use it }; const handleExportExcel = async () => { const XLSX = await import('xlsx'); // Use it }; ``` --- ## Summary **Performance Checklist:** - ✅ `useMemo` for expensive computations (filter, sort, map) - ✅ `useCallback` for functions passed to children - ✅ `React.memo` for expensive components - ✅ Debounce search/filter (300-500ms) - ✅ Cleanup timeouts/intervals in useEffect - ✅ Watch specific form fields (not all) - ✅ Stable keys in lists - ✅ Lazy load heavy libraries - ✅ Code splitting with React.lazy **See Also:** - [component-patterns.md](component-patterns.md) - Lazy loading - [data-fetching.md](data-fetching.md) - TanStack Query optimization - [complete-examples.md](complete-examples.md) - Performance patterns in context