useCallback Hook: Optimizing Function References

Preventing Unnecessary Re-renders with Memoized Callbacks

Welcome to useCallback!

Imagine you're a director giving the same instructions to actors repeatedly. Instead of explaining the entire scene each time, you could just say "do it like we rehearsed." useCallback is like creating a rehearsed version of a function - it remembers the function definition so child components don't think they're getting new instructions every time!

graph TD A[Parent Component Renders] --> B{Create Function} B --> C[Without useCallback: New Function Every Time] B --> D[With useCallback: Same Function Reference] C --> E[Child Component Sees New Prop] D --> F[Child Component Sees Same Prop] E --> G[Child Re-renders] F --> H[Child Skips Re-render] style C fill:#ff9999 style D fill:#99ff99 style H fill:#99ff99

Understanding useCallback

1. The Problem: Function Identity

// The Problem: Functions are recreated on every render
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
  
  // This function is recreated on every render!
  const handleClick = () => {
    console.log('Button clicked!');
  };
  
  return (
    <div>
      <input 
        value={text} 
        onChange={(e) => setText(e.target.value)} 
        placeholder="Type here..."
      />
      <div>Count: {count}</div>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      
      {/* ExpensiveChild re-renders whenever ParentComponent re-renders
          because handleClick is a new function each time */}
      <ExpensiveChild onClick={handleClick} />
    </div>
  );
}

// Even with React.memo, this component re-renders unnecessarily
const ExpensiveChild = React.memo(function ExpensiveChild({ onClick }) {
  console.log('ExpensiveChild rendered');
  
  // Simulate expensive operation
  const startTime = performance.now();
  while (performance.now() - startTime < 100) {
    // Artificial delay
  }
  
  return (
    <div className="expensive-child">
      <button onClick={onClick}>
        Click me (Expensive to render)
      </button>
    </div>
  );
});

// The Solution: useCallback
function OptimizedParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
  
  // This function is memoized and only recreated when dependencies change
  const handleClick = useCallback(() => {
    console.log('Button clicked!');
  }, []); // Empty dependency array = function never changes
  
  return (
    <div>
      <input 
        value={text} 
        onChange={(e) => setText(e.target.value)} 
        placeholder="Type here..."
      />
      <div>Count: {count}</div>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      
      {/* ExpensiveChild only re-renders when handleClick changes
          (which is never in this case) */}
      <ExpensiveChild onClick={handleClick} />
    </div>
  );
}

2. useCallback with Dependencies

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [selectedId, setSelectedId] = useState(null);
  
  // Bad: Function recreated on every render (including when selectedId changes)
  const badHandleSearch = () => {
    fetchResults(query).then(setResults);
  };
  
  // Better: Only recreated when query changes
  const handleSearch = useCallback(() => {
    fetchResults(query).then(setResults);
  }, [query]);
  
  // Function that depends on state
  const handleSelect = useCallback((id) => {
    setSelectedId(id);
    // Imagine we need to do something with the current query
    console.log(`Selected ${id} while searching for "${query}"`);
  }, [query]); // Recreated when query changes
  
  // Function that doesn't depend on external values
  const handleClear = useCallback(() => {
    setQuery('');
    setResults([]);
    setSelectedId(null);
  }, []); // Never recreated
  
  return (
    <div>
      <SearchInput
        value={query}
        onChange={setQuery}
        onSearch={handleSearch}
        onClear={handleClear}
      />
      <SearchResults
        results={results}
        selectedId={selectedId}
        onSelect={handleSelect}
      />
    </div>
  );
}

3. Common useCallback Patterns

// Pattern 1: Event Handlers
function FormComponent() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  
  // Memoized change handler
  const handleChange = useCallback((field) => (event) => {
    setFormData(prev => ({
      ...prev,
      [field]: event.target.value
    }));
  }, []);
  
  // Memoized submit handler
  const handleSubmit = useCallback(async (event) => {
    event.preventDefault();
    try {
      await submitForm(formData);
      // Reset form or show success message
    } catch (error) {
      // Handle error
    }
  }, [formData]);
  
  return (
    <form onSubmit={handleSubmit}>
      <FormField
        label="Name"
        value={formData.name}
        onChange={handleChange('name')}
      />
      <FormField
        label="Email"
        value={formData.email}
        onChange={handleChange('email')}
      />
      <FormField
        label="Message"
        value={formData.message}
        onChange={handleChange('message')}
        multiline
      />
      <button type="submit">Send</button>
    </form>
  );
}

// Pattern 2: API Calls
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  // Memoized fetch function
  const fetchUsers = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await api.getUsers();
      setUsers(response.data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, []); // No dependencies, API call doesn't depend on component state
  
  // Memoized delete handler
  const handleDelete = useCallback(async (userId) => {
    try {
      await api.deleteUser(userId);
      // Remove user from local state
      setUsers(prevUsers => prevUsers.filter(user => user.id !== userId));
    } catch (err) {
      setError(err.message);
    }
  }, []); // No dependencies needed
  
  // Fetch users on mount
  useEffect(() => {
    fetchUsers();
  }, [fetchUsers]);
  
  return (
    <div>
      {loading && <Spinner />}
      {error && <ErrorMessage message={error} />}
      <UserListDisplay
        users={users}
        onDelete={handleDelete}
        onRefresh={fetchUsers}
      />
    </div>
  );
}

// Pattern 3: Debounced Functions
function SearchableList({ items }) {
  const [searchTerm, setSearchTerm] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  
  // Create debounced search function
  const debouncedSearch = useCallback(
    debounce((term) => {
      const filtered = items.filter(item =>
        item.name.toLowerCase().includes(term.toLowerCase())
      );
      setFilteredItems(filtered);
    }, 300),
    [items] // Recreate when items change
  );
  
  // Handle search input change
  const handleSearchChange = useCallback((event) => {
    const value = event.target.value;
    setSearchTerm(value);
    debouncedSearch(value);
  }, [debouncedSearch]);
  
  return (
    <div>
      <SearchInput
        value={searchTerm}
        onChange={handleSearchChange}
        placeholder="Search items..."
      />
      <ItemList items={filteredItems} />
    </div>
  );
}

Advanced useCallback Patterns

1. Callback Composition

function AdvancedForm() {
  const [formData, setFormData] = useState({});
  const [validation, setValidation] = useState({});
  const [submitting, setSubmitting] = useState(false);
  
  // Base update function
  const updateField = useCallback((field, value) => {
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
  }, []);
  
  // Validation function
  const validateField = useCallback((field, value) => {
    const errors = {};
    switch (field) {
      case 'email':
        if (!value.includes('@')) {
          errors.email = 'Invalid email address';
        }
        break;
      case 'password':
        if (value.length < 8) {
          errors.password = 'Password must be at least 8 characters';
        }
        break;
      // ... other validations
    }
    setValidation(prev => ({
      ...prev,
      ...errors
    }));
    return Object.keys(errors).length === 0;
  }, []);
  
  // Composed update with validation
  const updateFieldWithValidation = useCallback((field) => (event) => {
    const value = event.target.value;
    updateField(field, value);
    validateField(field, value);
  }, [updateField, validateField]);
  
  // Submit handler with validation
  const handleSubmit = useCallback(async (event) => {
    event.preventDefault();
    
    // Validate all fields
    const isValid = Object.keys(formData).every(field =>
      validateField(field, formData[field])
    );
    
    if (!isValid) return;
    
    setSubmitting(true);
    try {
      await api.submitForm(formData);
      // Handle success
    } catch (error) {
      // Handle error
    } finally {
      setSubmitting(false);
    }
  }, [formData, validateField]);
  
  return (
    <form onSubmit={handleSubmit}>
      <FormField
        name="email"
        value={formData.email || ''}
        onChange={updateFieldWithValidation('email')}
        error={validation.email}
      />
      <FormField
        name="password"
        type="password"
        value={formData.password || ''}
        onChange={updateFieldWithValidation('password')}
        error={validation.password}
      />
      <button type="submit" disabled={submitting}>
        {submitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

2. Dynamic Callbacks

function DynamicList({ items, renderItem }) {
  const [selectedIds, setSelectedIds] = useState(new Set());
  const [sortConfig, setSortConfig] = useState({ key: 'name', direction: 'asc' });
  
  // Create item-specific callbacks dynamically
  const createItemCallbacks = useCallback((item) => ({
    onSelect: () => {
      setSelectedIds(prev => {
        const next = new Set(prev);
        if (next.has(item.id)) {
          next.delete(item.id);
        } else {
          next.add(item.id);
        }
        return next;
      });
    },
    onEdit: () => {
      // Navigate to edit page or open modal
      console.log('Editing item:', item.id);
    },
    onDelete: async () => {
      if (window.confirm('Are you sure?')) {
        try {
          await api.deleteItem(item.id);
          // Update local state
        } catch (error) {
          console.error('Delete failed:', error);
        }
      }
    }
  }), []);
  
  // Memoize the callback creator itself
  const getItemProps = useCallback((item) => {
    const callbacks = createItemCallbacks(item);
    return {
      item,
      isSelected: selectedIds.has(item.id),
      ...callbacks
    };
  }, [selectedIds, createItemCallbacks]);
  
  // Sort items
  const sortedItems = useMemo(() => {
    return [...items].sort((a, b) => {
      const aValue = a[sortConfig.key];
      const bValue = b[sortConfig.key];
      const direction = sortConfig.direction === 'asc' ? 1 : -1;
      
      if (aValue < bValue) return -1 * direction;
      if (aValue > bValue) return 1 * direction;
      return 0;
    });
  }, [items, sortConfig]);
  
  return (
    <div>
      <SortControls
        config={sortConfig}
        onChange={setSortConfig}
      />
      <div className="item-list">
        {sortedItems.map(item => (
          <MemoizedItem
            key={item.id}
            {...getItemProps(item)}
            render={renderItem}
          />
        ))}
      </div>
    </div>
  );
}

// Memoized item component
const MemoizedItem = React.memo(function Item({ item, isSelected, onSelect, onEdit, onDelete, render }) {
  return render({ item, isSelected, onSelect, onEdit, onDelete });
});

3. Callback with Refs

function VideoPlayer({ src }) {
  const videoRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  
  // Callbacks that interact with refs
  const play = useCallback(() => {
    if (videoRef.current) {
      videoRef.current.play();
      setIsPlaying(true);
    }
  }, []);
  
  const pause = useCallback(() => {
    if (videoRef.current) {
      videoRef.current.pause();
      setIsPlaying(false);
    }
  }, []);
  
  const seek = useCallback((time) => {
    if (videoRef.current) {
      videoRef.current.currentTime = time;
      setCurrentTime(time);
    }
  }, []);
  
  const togglePlay = useCallback(() => {
    if (isPlaying) {
      pause();
    } else {
      play();
    }
  }, [isPlaying, play, pause]);
  
  // Event handlers
  const handleTimeUpdate = useCallback(() => {
    if (videoRef.current) {
      setCurrentTime(videoRef.current.currentTime);
    }
  }, []);
  
  const handleLoadedMetadata = useCallback(() => {
    if (videoRef.current) {
      setDuration(videoRef.current.duration);
    }
  }, []);
  
  return (
    <div className="video-player">
      <video
        ref={videoRef}
        src={src}
        onTimeUpdate={handleTimeUpdate}
        onLoadedMetadata={handleLoadedMetadata}
        onPlay={() => setIsPlaying(true)}
        onPause={() => setIsPlaying(false)}
      />
      <VideoControls
        isPlaying={isPlaying}
        currentTime={currentTime}
        duration={duration}
        onPlayPause={togglePlay}
        onSeek={seek}
      />
    </div>
  );
}

Performance Patterns with useCallback

1. Optimizing List Rendering

function OptimizedList({ items }) {
  const [selectedId, setSelectedId] = useState(null);
  
  // Memoize the selection handler
  const handleSelect = useCallback((id) => {
    setSelectedId(id);
  }, []);
  
  return (
    <div>
      {items.map(item => (
        <OptimizedListItem
          key={item.id}
          item={item}
          isSelected={item.id === selectedId}
          onSelect={handleSelect}
        />
      ))}
    </div>
  );
}

// Optimized list item with React.memo
const OptimizedListItem = React.memo(function ListItem({ 
  item, 
  isSelected, 
  onSelect 
}) {
  console.log(`Rendering item ${item.id}`);
  
  // Create a stable callback for this specific item
  const handleClick = useCallback(() => {
    onSelect(item.id);
  }, [item.id, onSelect]);
  
  return (
    <div 
      className={`list-item ${isSelected ? 'selected' : ''}`}
      onClick={handleClick}
    >
      <h3>{item.title}</h3>
      <p>{item.description}</p>
    </div>
  );
});

2. Optimizing Context Providers

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);
  
  // Memoize all context methods
  const login = useCallback(async (credentials) => {
    try {
      const userData = await api.login(credentials);
      setUser(userData);
      return userData;
    } catch (error) {
      throw error;
    }
  }, []);
  
  const logout = useCallback(() => {
    setUser(null);
    // Clear other auth-related state
  }, []);
  
  const toggleTheme = useCallback(() => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  }, []);
  
  const addNotification = useCallback((notification) => {
    const id = Date.now();
    setNotifications(prev => [...prev, { ...notification, id }]);
    
    // Auto-dismiss after 5 seconds
    setTimeout(() => {
      removeNotification(id);
    }, 5000);
  }, []);
  
  const removeNotification = useCallback((id) => {
    setNotifications(prev => prev.filter(n => n.id !== id));
  }, []);
  
  // Memoize the context value
  const contextValue = useMemo(() => ({
    user,
    theme,
    notifications,
    login,
    logout,
    toggleTheme,
    addNotification,
    removeNotification
  }), [
    user, 
    theme, 
    notifications, 
    login, 
    logout, 
    toggleTheme, 
    addNotification, 
    removeNotification
  ]);
  
  return (
    <AppContext.Provider value={contextValue}>
      {children}
    </AppContext.Provider>
  );
}

3. Optimizing Event Delegation

function DataTable({ rows, columns }) {
  const [sortConfig, setSortConfig] = useState(null);
  const [selectedRows, setSelectedRows] = useState(new Set());
  
  // Single event handler for all cell clicks
  const handleCellClick = useCallback((event) => {
    const cell = event.target.closest('td');
    if (!cell) return;
    
    const rowId = cell.parentElement.dataset.rowId;
    const columnId = cell.dataset.columnId;
    
    // Handle different types of clicks
    if (cell.classList.contains('selectable')) {
      setSelectedRows(prev => {
        const next = new Set(prev);
        if (next.has(rowId)) {
          next.delete(rowId);
        } else {
          next.add(rowId);
        }
        return next;
      });
    } else if (cell.classList.contains('sortable')) {
      setSortConfig(prev => ({
        column: columnId,
        direction: prev?.column === columnId && prev.direction === 'asc' 
          ? 'desc' 
          : 'asc'
      }));
    }
  }, []);
  
  // Memoize sorted rows
  const sortedRows = useMemo(() => {
    if (!sortConfig) return rows;
    
    return [...rows].sort((a, b) => {
      const aValue = a[sortConfig.column];
      const bValue = b[sortConfig.column];
      const direction = sortConfig.direction === 'asc' ? 1 : -1;
      
      if (aValue < bValue) return -1 * direction;
      if (aValue > bValue) return 1 * direction;
      return 0;
    });
  }, [rows, sortConfig]);
  
  return (
    <table onClick={handleCellClick}>
      <thead>
        <tr>
          <th className="selectable">
            <input
              type="checkbox"
              checked={selectedRows.size === rows.length}
              onChange={(e) => {
                if (e.target.checked) {
                  setSelectedRows(new Set(rows.map(r => r.id)));
                } else {
                  setSelectedRows(new Set());
                }
              }}
            />
          </th>
          {columns.map(column => (
            <th
              key={column.id}
              data-column-id={column.id}
              className={column.sortable ? 'sortable' : ''}
            >
              {column.label}
              {sortConfig?.column === column.id && (
                <span>{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>
              )}
            </th>
          ))}
        </tr>
      </thead>
      <tbody>
        {sortedRows.map(row => (
          <tr key={row.id} data-row-id={row.id}>
            <td className="selectable">
              <input
                type="checkbox"
                checked={selectedRows.has(row.id)}
                readOnly
              />
            </td>
            {columns.map(column => (
              <td key={column.id} data-column-id={column.id}>
                {row[column.id]}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Common Pitfalls and Solutions

1. Missing Dependencies

// PROBLEM: Missing dependency causes stale closure
function ProblemComponent({ userId }) {
  const [userData, setUserData] = useState(null);
  
  // Missing userId in dependencies!
  const fetchUser = useCallback(async () => {
    const data = await api.getUser(userId);
    setUserData(data);
  }, []); // Should include userId
  
  useEffect(() => {
    fetchUser();
  }, [fetchUser]);
  
  // If userId changes, fetchUser still uses the old value!
  
  // SOLUTION: Include all dependencies
  const fetchUserFixed = useCallback(async () => {
    const data = await api.getUser(userId);
    setUserData(data);
  }, [userId]); // Include userId in dependencies
}

// Better solution: Pass parameters directly
function BetterComponent({ userId }) {
  const [userData, setUserData] = useState(null);
  
  const fetchUser = useCallback(async (id) => {
    const data = await api.getUser(id);
    setUserData(data);
  }, []); // No dependencies needed
  
  useEffect(() => {
    fetchUser(userId);
  }, [fetchUser, userId]);
}

2. Over-using useCallback

// PROBLEM: Unnecessary useCallback
function OverOptimized() {
  const [count, setCount] = useState(0);
  
  // These are unnecessary!
  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []);
  
  const decrement = useCallback(() => {
    setCount(c => c - 1);
  }, []);
  
  const reset = useCallback(() => {
    setCount(0);
  }, []);
  
  // Simple inline functions are fine here
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <button onClick={() => setCount(c => c - 1)}>-</button>
      <button onClick={() => setCount(0)}>Reset</button>
      <div>Count: {count}</div>
    </div>
  );
}

// SOLUTION: Only use useCallback when necessary
function Optimized() {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState([]);
  
  // Only memoize if passed to optimized child components
  const handleItemClick = useCallback((id) => {
    console.log('Item clicked:', id);
  }, []);
  
  return (
    <div>
      <div>
        <button onClick={() => setCount(c => c + 1)}>+</button>
        <div>Count: {count}</div>
      </div>
      <ExpensiveList items={items} onItemClick={handleItemClick} />
    </div>
  );
}

3. Dependencies on Objects/Arrays

// PROBLEM: Object/array dependencies
function ProblemComponent({ config }) {
  // This will recreate on every render if config object changes reference
  const processData = useCallback((data) => {
    return data.filter(item => item.value > config.threshold);
  }, [config]); // Entire config object as dependency
  
  // SOLUTION 1: Destructure needed values
  const processDataFixed = useCallback((data) => {
    return data.filter(item => item.value > config.threshold);
  }, [config.threshold]); // Only the specific value needed
  
  // SOLUTION 2: Pass values as parameters
  const processDataBetter = useCallback((data, threshold) => {
    return data.filter(item => item.value > threshold);
  }, []); // No dependencies needed
  
  // Use like this:
  const result = processDataBetter(data, config.threshold);
}

Testing Components with useCallback

import { render, fireEvent, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// Component to test
function SearchForm({ onSearch }) {
  const [query, setQuery] = useState('');
  
  const handleSubmit = useCallback((e) => {
    e.preventDefault();
    onSearch(query);
  }, [query, onSearch]);
  
  const handleChange = useCallback((e) => {
    setQuery(e.target.value);
  }, []);
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={query}
        onChange={handleChange}
        placeholder="Search..."
      />
      <button type="submit">Search</button>
    </form>
  );
}

// Tests
describe('SearchForm', () => {
  it('calls onSearch with query when submitted', async () => {
    const handleSearch = jest.fn();
    render(<SearchForm onSearch={handleSearch} />);
    
    const input = screen.getByPlaceholderText('Search...');
    const button = screen.getByText('Search');
    
    // Type in the input
    await userEvent.type(input, 'test query');
    
    // Submit the form
    fireEvent.click(button);
    
    expect(handleSearch).toHaveBeenCalledWith('test query');
  });
  
  it('maintains callback reference stability', () => {
    const handleSearch = jest.fn();
    const { rerender } = render(<SearchForm onSearch={handleSearch} />);
    
    // Store initial onChange reference
    const input = screen.getByPlaceholderText('Search...');
    const initialOnChange = input.props.onChange;
    
    // Rerender with same props
    rerender(<SearchForm onSearch={handleSearch} />);
    
    // onChange should be the same reference
    expect(input.props.onChange).toBe(initialOnChange);
  });
});

// Testing memoization effectiveness
function MemoizedChild({ onClick }) {
  const renderCount = useRef(0);
  renderCount.current++;
  
  return (
    <button onClick={onClick}>
      Rendered {renderCount.current} times
    </button>
  );
}

describe('useCallback memoization', () => {
  it('prevents unnecessary re-renders of child components', () => {
    function Parent() {
      const [count, setCount] = useState(0);
      const handleClick = useCallback(() => {
        console.log('clicked');
      }, []);
      
      return (
        <div>
          <button onClick={() => setCount(c => c + 1)}>
            Increment: {count}
          </button>
          <MemoizedChild onClick={handleClick} />
        </div>
      );
    }
    
    render(<Parent />);
    
    const incrementButton = screen.getByText(/Increment:/);
    const memoizedButton = screen.getByText(/Rendered 1 times/);
    
    // Click increment button multiple times
    fireEvent.click(incrementButton);
    fireEvent.click(incrementButton);
    
    // Child should still show only 1 render
    expect(memoizedButton).toHaveTextContent('Rendered 1 times');
  });
});

Best Practices

1. Only Memoize When Necessary

// Only use useCallback when:
// 1. Passing to optimized child components
// 2. The function is expensive to create
// 3. Used in dependency arrays of other hooks

// Good use case
const ParentComponent = React.memo(({ children, onAction }) => {
  // Memoized because passed to memo component
  const handleAction = useCallback(() => {
    onAction();
  }, [onAction]);
  
  return <ChildComponent onAction={handleAction} />;
});

// Unnecessary use case
function SimpleComponent() {
  // Don't memoize simple event handlers
  return <button onClick={() => console.log('clicked')}>Click</button>;
}

2. Keep Dependencies Minimal

// Instead of object dependencies
const handleFilter = useCallback((data) => {
  return data.filter(item => item.value > config.threshold);
}, [config]); // Entire object as dependency

// Use specific values
const handleFilter = useCallback((data) => {
  return data.filter(item => item.value > config.threshold);
}, [config.threshold]); // Only what you need

// Or pass as parameters
const handleFilter = useCallback((data, threshold) => {
  return data.filter(item => item.value > threshold);
}, []); // No dependencies

3. Combine with useMemo and React.memo

const MemoizedComponent = React.memo(({ data, onAction }) => {
  const processedData = useMemo(() => {
    return expensiveOperation(data);
  }, [data]);
  
  return (
    <div>
      {processedData.map(item => (
        <div key={item.id} onClick={() => onAction(item.id)}>
          {item.name}
        </div>
      ))}
    </div>
  );
});

function Parent() {
  const [data, setData] = useState([]);
  
  const handleAction = useCallback((id) => {
    console.log('Action on item:', id);
  }, []);
  
  return <MemoizedComponent data={data} onAction={handleAction} />;
}

Practice Exercises

Exercise 1: Optimize a Form

Create a form with multiple fields that:

  • Prevents unnecessary re-renders of field components
  • Memoizes validation functions
  • Optimizes submit handler
  • Handles field-specific onChange handlers efficiently

Exercise 2: Build an Infinite Scroll List

Create a list component that:

  • Loads more items when scrolling to bottom
  • Memoizes item click handlers
  • Prevents re-renders of existing items
  • Handles selection state efficiently

Exercise 3: Create a Data Grid

Build a data grid component with:

  • Sortable columns
  • Row selection
  • Cell editing
  • Optimized event handlers using useCallback

Key Takeaways