Advanced Hooks Patterns
Explore advanced Hook patterns and performance optimization techniques.
useContext Hook: Solving the Props Drilling Problem
The useContext hook is one of React's most powerful tools for sharing data across your component tree. Before diving into the technical details, let's understand the problem it solves and why it's so important for building maintainable React applications.
The Props Drilling Problem Imagine you're building a large application where user information needs to be displayed in many different components throughout your app. In a traditional React setup, you'd need to pass this user data down through props from the top-level component to every component that needs it, even if intermediate components don't use the data themselves.
This creates a situation called "props drilling" - where you're passing props down through multiple layers of components just to get data to where it's actually needed. It's like having to pass a message through 10 people to reach the 11th person, even though people 2-10 don't care about the message.
What is Context? React Context is like a global storage system that any component can access without needing props passed down from parents. Think of it as a shared locker that any component in your app can open, as long as they have the key (the useContext hook).
When to Use useContext Context is perfect for data that many components need to access:
- Theme preferences (light/dark mode)
- User authentication (current user info, login status)
- Language settings (for internationalization)
- Shopping cart contents (in e-commerce apps)
- Application settings (API endpoints, feature flags)
When NOT to Use useContext Context isn't a replacement for all prop passing. Don't use it for:
- Component-specific data that only one or two components need
- Frequently changing data that would cause many re-renders
- Data that should be encapsulated within a specific component tree
The Context Pattern: Provider and Consumer Context works with two main parts:
- Provider: A component that "provides" the data to its children
- Consumer: Components that "consume" or use the data (via useContext hook)
Real-World Analogy Think of Context like a radio station:
- The Provider is the radio tower broadcasting information
- Consumers (components using useContext) are radios tuned in to that station
- Any radio in range can receive the broadcast without needing a direct wire connection
- When the broadcast changes, all tuned-in radios get the update automatically
Performance Considerations When the value in a Context changes, ALL components consuming that context will re-render. This is why it's important to structure your contexts thoughtfully and sometimes split them into multiple contexts for different types of data.
1// 🌟 useContext: Theme System Example2import { createContext, useContext, useState, useEffect } from 'react';34// Step 1: Create Context5const ThemeContext = createContext({6 theme: 'light',7 toggleTheme: () => {},8 colors: {}9});1011// Step 2: Provider Component12function ThemeProvider({ children }) {13 const [theme, setTheme] = useState(() =>14 localStorage.getItem('theme') || 'light'15 );1617 const themes = {18 light: { bg: '#fff', text: '#333', primary: '#007bff' },19 dark: { bg: '#212529', text: '#fff', primary: '#0d6efd' }20 };2122 const toggleTheme = () => {23 const newTheme = theme === 'light' ? 'dark' : 'light';24 setTheme(newTheme);25 localStorage.setItem('theme', newTheme);26 };2728 return (29 <ThemeContext.Provider value={{30 theme,31 toggleTheme,32 colors: themes[theme]33 }}>34 {children}35 </ThemeContext.Provider>36 );37}3839// Step 3: Custom Hook40function useTheme() {41 const context = useContext(ThemeContext);42 if (!context) {43 throw new Error('useTheme must be used within ThemeProvider');44 }45 return context;46}4748// Step 4: Using the Context49function Header() {50 const { theme, toggleTheme, colors } = useTheme();5152 return (53 <header style={{ backgroundColor: colors.bg, color: colors.text }}>54 <h1>My App</h1>55 <button onClick={toggleTheme}>56 {theme === 'light' ? '🌙' : '☀️'} Toggle Theme57 </button>58 </header>59 );60}6162function Card({ title, children }) {63 const { colors } = useTheme();6465 return (66 <div style={{67 backgroundColor: colors.bg,68 color: colors.text,69 border: `1px solid ${colors.primary}`70 }}>71 <h3>{title}</h3>72 {children}73 </div>74 );75}7677// Main App78function App() {79 return (80 <ThemeProvider>81 <Header />82 <main>83 <Card title="Welcome">84 <p>Context eliminates prop drilling!</p>85 </Card>86 </main>87 </ThemeProvider>88 );89}9091// 💡 KEY POINTS:92// - Context provides global state without prop drilling93// - Always use custom hooks for better error handling94// - Split contexts for better performance95// - Don't overuse - props are still useful!
useReducer Hook: Advanced State Management Made Simple
The useReducer hook is React's answer to managing complex state logic in a predictable way. While useState is perfect for simple state, useReducer shines when you have complex state logic with multiple sub-values or when the next state depends on the previous one.
Why Does useReducer Exist? As applications grow, state management can become increasingly complex. You might find yourself with:
- Multiple useState calls that are related to each other
- Complex state update logic scattered throughout your component
- State updates that depend on multiple pieces of the current state
- The need to pass multiple state setters down to child components
useReducer solves these problems by centralizing your state logic in one place and making state updates predictable.
The Mental Model: Think Like a Bank Account A great way to understand useReducer is to think of it like a bank account system:
- State is your account balance and transaction history
- Actions are the different types of transactions (deposit, withdraw, transfer)
- Reducer is the bank teller who processes these transactions according to strict rules
- Dispatch is how you submit transaction requests to the teller
Just like a bank teller follows specific procedures for each transaction type, a reducer follows specific logic for each action type.
Core Concepts of useReducer
- State: An object containing all your component's data
- Action: An object describing what happened (usually has a 'type' property)
- Reducer: A pure function that takes the current state and an action, then returns the new state
- Dispatch: A function that sends actions to the reducer
When to Use useReducer vs useState Use useReducer when:
- State logic is complex with multiple sub-values
- The next state depends on the previous state
- You want to optimize performance for components that trigger deep updates
- You need to pass state logic down to child components
- Testing complex state logic separately from components
Use useState when:
- Managing independent, simple state values
- State updates are straightforward
- You don't need centralized state logic
The Power of Predictability With useReducer, every state change happens through the reducer function. This means:
- You can log every action to see exactly what's happening
- State changes are predictable and testable
- You can implement undo/redo functionality easily
- Debugging is much easier because all state logic is in one place
1// 🛒 useReducer: Shopping Cart Example2import { useReducer, createContext, useContext } from 'react';34// Action types5const ACTIONS = {6 ADD_ITEM: 'ADD_ITEM',7 REMOVE_ITEM: 'REMOVE_ITEM',8 UPDATE_QTY: 'UPDATE_QTY',9 CLEAR_CART: 'CLEAR_CART'10};1112// Initial state13const initialState = {14 items: [],15 total: 016};1718// Reducer function - all state logic in one place19function cartReducer(state, action) {20 switch (action.type) {21 case ACTIONS.ADD_ITEM: {22 const existing = state.items.find(item => item.id === action.payload.id);2324 if (existing) {25 return {26 ...state,27 items: state.items.map(item =>28 item.id === action.payload.id29 ? { ...item, quantity: item.quantity + 1 }30 : item31 )32 };33 }3435 return {36 ...state,37 items: [...state.items, { ...action.payload, quantity: 1 }]38 };39 }4041 case ACTIONS.REMOVE_ITEM:42 return {43 ...state,44 items: state.items.filter(item => item.id !== action.payload)45 };4647 case ACTIONS.UPDATE_QTY:48 return {49 ...state,50 items: state.items.map(item =>51 item.id === action.payload.id52 ? { ...item, quantity: action.payload.quantity }53 : item54 ).filter(item => item.quantity > 0)55 };5657 case ACTIONS.CLEAR_CART:58 return initialState;5960 default:61 return state;62 }63}6465// Cart Context66const CartContext = createContext(null);6768function CartProvider({ children }) {69 const [state, dispatch] = useReducer(cartReducer, initialState);7071 // Calculate total72 const total = state.items.reduce(73 (sum, item) => sum + item.price * item.quantity,74 075 );7677 // Action creators78 const addItem = (item) => dispatch({ type: ACTIONS.ADD_ITEM, payload: item });79 const removeItem = (id) => dispatch({ type: ACTIONS.REMOVE_ITEM, payload: id });80 const updateQuantity = (id, quantity) =>81 dispatch({ type: ACTIONS.UPDATE_QTY, payload: { id, quantity } });82 const clearCart = () => dispatch({ type: ACTIONS.CLEAR_CART });8384 return (85 <CartContext.Provider value={{86 items: state.items,87 total,88 addItem,89 removeItem,90 updateQuantity,91 clearCart92 }}>93 {children}94 </CartContext.Provider>95 );96}9798// Custom hook99function useCart() {100 const context = useContext(CartContext);101 if (!context) throw new Error('useCart must be used within CartProvider');102 return context;103}104105// Usage Example106function ProductCard({ product }) {107 const { addItem } = useCart();108109 return (110 <div>111 <h3>{product.name}</h3>112 <p>\$\{product.price\}</p>113 <button onClick={() => addItem(product)}>Add to Cart</button>114 </div>115 );116}117118function Cart() {119 const { items, total, updateQuantity, removeItem, clearCart } = useCart();120121 if (items.length === 0) return <p>Cart is empty</p>;122123 return (124 <div>125 <h2>Shopping Cart</h2>126 {items.map(item => (127 <div key={item.id}>128 <span>{item.name} - \$\{item.price\}</span>129 <button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>130 <span>{item.quantity}</span>131 <button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>132 <button onClick={() => removeItem(item.id)}>Remove</button>133 </div>134 ))}135 <p>Total: ${total.toFixed(2)}</p>136 <button onClick={clearCart}>Clear Cart</button>137 </div>138 );139}140141// 💡 KEY BENEFITS:142// - Centralized state logic143// - Predictable updates144// - Easy to test145// - Perfect for complex state
Performance Optimization with useMemo: Making Your React App Lightning Fast
The useMemo hook is your secret weapon for optimizing React applications by preventing expensive calculations from running on every render. Understanding when and how to use useMemo can make the difference between a sluggish app and one that feels instantaneous.
The Problem useMemo Solves In React, components re-render whenever their state or props change. During each render, all the code in your component runs again, including any calculations or data transformations. If these calculations are expensive (take a long time), your app can feel slow and unresponsive.
Real-World Analogy: The Smart Calculator Imagine you're a math tutor who needs to calculate complex problems for students. Without useMemo, you'd recalculate every problem from scratch each time a student asks, even if they're asking about the same problem you just solved. With useMemo, you'd write down the answers and only recalculate when the problem actually changes.
When Expensive Calculations Matter "Expensive" calculations include:
- Filtering or sorting large arrays
- Complex mathematical computations
- Creating new objects from existing data
- Processing text (parsing, formatting)
- Generating charts or visualizations
The Syntax and Mental Model useMemo takes two arguments:
- A function that returns the calculated value
- An array of dependencies (values that, when changed, trigger recalculation)
Think of useMemo as a smart cache that remembers results and only recalculates when necessary.
When to Use useMemo Use useMemo when:
- You have expensive calculations that don't need to run on every render
- You're passing objects/arrays to child components that use React.memo
- You're experiencing performance issues verified by profiling
- Calculations depend on specific props/state values
When NOT to Use useMemo Don't use useMemo for:
- Simple calculations (the overhead of useMemo might be worse)
- Values that change on every render anyway
- Premature optimization (measure first!)
- Side effects (use useEffect instead)
Common Pitfalls and How to Avoid Them
- Forgetting dependencies: Always include all variables used in the calculation
- Over-memoizing: Not everything needs to be memoized
- Memoizing primitives: Usually unnecessary for strings, numbers, booleans
- Breaking referential equality: Creating new functions/objects in the dependency array
1// 🚀 useMemo: Performance Optimization Examples2import { useState, useMemo, memo } from 'react';34// Example 1: Expensive Calculation5function PrimeCalculator() {6 const [count, setCount] = useState(1000);7 const [dark, setDark] = useState(false);89 // ✅ Only recalculates when count changes10 const primes = useMemo(() => {11 console.log('Calculating primes...');12 const result = [];13 for (let i = 2; i <= count; i++) {14 if (isPrime(i)) result.push(i);15 }16 return result;17 }, [count]);1819 return (20 <div style={{ background: dark ? '#333' : '#fff', color: dark ? '#fff' : '#000' }}>21 <h3>Found {primes.length} prime numbers</h3>22 <button onClick={() => setCount(c => c + 1000)}>More Primes</button>23 <button onClick={() => setDark(d => !d)}>Toggle Theme</button>24 </div>25 );26}2728function isPrime(n) {29 for (let i = 2; i <= Math.sqrt(n); i++) {30 if (n % i === 0) return false;31 }32 return n > 1;33}3435// Example 2: Filtering Large Lists36function ProductList() {37 const [filter, setFilter] = useState('');38 const [sort, setSort] = useState('name');3940 // Generate products once41 const products = useMemo(() =>42 Array.from({ length: 1000 }, (_, i) => ({43 id: i,44 name: `Product ${i}`,45 price: Math.random() * 100,46 category: ['Electronics', 'Books', 'Clothing'][i % 3]47 })), []48 );4950 // Filter and sort products51 const displayProducts = useMemo(() => {52 console.log('Filtering products...');53 return products54 .filter(p => p.name.toLowerCase().includes(filter.toLowerCase()))55 .sort((a, b) => {56 if (sort === 'name') return a.name.localeCompare(b.name);57 if (sort === 'price') return a.price - b.price;58 return 0;59 });60 }, [products, filter, sort]);6162 return (63 <div>64 <input65 placeholder="Search..."66 value={filter}67 onChange={e => setFilter(e.target.value)}68 />69 <select value={sort} onChange={e => setSort(e.target.value)}>70 <option value="name">Name</option>71 <option value="price">Price</option>72 </select>73 <p>Showing {displayProducts.length} products</p>74 {/* Render first 10 products */}75 {displayProducts.slice(0, 10).map(p => (76 <div key={p.id}>{p.name} - ${p.price.toFixed(2)}</div>77 ))}78 </div>79 );80}8182// Example 3: Stable Object References83function Parent() {84 const [count, setCount] = useState(0);85 const [name, setName] = useState('');8687 // ✅ Object reference stays stable unless name changes88 const config = useMemo(() => ({89 theme: 'dark',90 user: name,91 features: ['dashboard', 'analytics']92 }), [name]);9394 return (95 <div>96 <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>97 <input value={name} onChange={e => setName(e.target.value)} />98 <ExpensiveChild config={config} />99 </div>100 );101}102103const ExpensiveChild = memo(({ config }) => {104 console.log('ExpensiveChild rendered');105 return <div>Theme: {config.theme}, User: {config.user}</div>;106});107108// 💡 WHEN TO USE useMemo:109// - Expensive calculations (sorting, filtering, computing)110// - Creating objects/arrays passed to memoized children111// - Complex data transformations112//113// ⚠️ DON'T USE FOR:114// - Simple calculations115// - Primitive values116// - Every single value (premature optimization)
Function Optimization with useCallback: Preventing Unnecessary Re-renders
The useCallback hook is your tool for optimizing function references in React components. While useMemo memoizes values, useCallback specifically memoizes functions, preventing them from being recreated on every render.
The Function Reference Problem In JavaScript, functions are objects. Every time a component re-renders, any functions defined inside it are recreated, resulting in new function references. This means that even if the function does exactly the same thing, React sees it as a "different" function.
This becomes problematic when:
- Passing functions as props to child components (especially memoized ones)
- Using functions as dependencies in useEffect or other hooks
- Dealing with expensive child component re-renders
Real-World Analogy: The Meeting Room Problem Imagine you're scheduling daily meetings in the same room. Without useCallback, it's like creating a brand new meeting room every day, even though you're discussing the same topics with the same people. With useCallback, you're reusing the same meeting room and only creating a new one when the meeting agenda (dependencies) actually changes.
Understanding Referential Equality In React, when comparing props to determine if a component should re-render:
- Primitive values (numbers, strings, booleans) are compared by value
- Objects and functions are compared by reference
This means two functions that do the exact same thing are considered different if they're different objects in memory.
When to Use useCallback Use useCallback when:
- Passing callbacks to optimized child components that rely on reference equality
- The function is a dependency of other hooks
- You have expensive child components that re-render frequently
- Creating the function itself is computationally expensive
When NOT to Use useCallback Don't use useCallback for:
- Simple event handlers in components without optimization needs
- Functions that are supposed to be recreated (capture fresh values)
- Internal functions that aren't passed anywhere
- Premature optimization without measured performance issues
The Relationship with React.memo useCallback shines brightest when combined with React.memo. React.memo prevents re-renders when props haven't changed, but without useCallback, function props would always appear to change.
Common Patterns and Best Practices
- Stable event handlers: Keep function references stable across renders
- Callback with state updates: Use functional updates to avoid stale closures
- Complex event handling: Memoize handlers that perform multiple operations
- API calls and effects: Prevent infinite loops in useEffect
1// 🎯 useCallback: Function Optimization Examples2import { useState, useCallback, useEffect, memo } from 'react';34// Example 1: Basic Function Memoization5function TodoApp() {6 const [count, setCount] = useState(0);7 const [todos, setTodos] = useState([]);89 // ✅ Stable function reference10 const addTodo = useCallback(() => {11 setTodos(prev => [...prev, {12 id: Date.now(),13 text: `Todo ${prev.length + 1}`14 }]);15 }, []); // Empty deps = never recreated1617 const removeTodo = useCallback((id) => {18 setTodos(prev => prev.filter(todo => todo.id !== id));19 }, []); // Functional update = no deps needed2021 return (22 <div>23 <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>24 <TodoList todos={todos} onAdd={addTodo} onRemove={removeTodo} />25 </div>26 );27}2829// Memoized child - only re-renders when props change30const TodoList = memo(({ todos, onAdd, onRemove }) => {31 console.log('TodoList rendered');32 return (33 <div>34 <button onClick={onAdd}>Add Todo</button>35 {todos.map(todo => (36 <div key={todo.id}>37 {todo.text}38 <button onClick={() => onRemove(todo.id)}>X</button>39 </div>40 ))}41 </div>42 );43});4445// Example 2: useCallback with useEffect46function SearchBox() {47 const [query, setQuery] = useState('');48 const [results, setResults] = useState([]);4950 // ✅ Stable search function for useEffect51 const search = useCallback(async (searchQuery) => {52 if (!searchQuery) {53 setResults([]);54 return;55 }5657 console.log(`Searching for: ${searchQuery}`);58 // Simulate API call59 await new Promise(r => setTimeout(r, 300));60 setResults([`Result for ${searchQuery}`, `Another ${searchQuery} result`]);61 }, []);6263 // Debounced search64 useEffect(() => {65 const timer = setTimeout(() => search(query), 500);66 return () => clearTimeout(timer);67 }, [query, search]); // search is stable!6869 return (70 <div>71 <input72 value={query}73 onChange={e => setQuery(e.target.value)}74 placeholder="Search..."75 />76 {results.map((r, i) => <div key={i}>{r}</div>)}77 </div>78 );79}8081// Example 3: Complex Event Handlers82function UserList() {83 const [users, setUsers] = useState([84 { id: 1, name: 'Alice', active: true },85 { id: 2, name: 'Bob', active: false }86 ]);8788 // ✅ Generic update function89 const updateUser = useCallback((id, updates) => {90 setUsers(prev => prev.map(user =>91 user.id === id ? { ...user, ...updates } : user92 ));93 }, []);9495 // ✅ Specific action handlers96 const toggleActive = useCallback((id) => {97 setUsers(prev => prev.map(user =>98 user.id === id ? { ...user, active: !user.active } : user99 ));100 }, []);101102 return (103 <div>104 {users.map(user => (105 <UserCard106 key={user.id}107 user={user}108 onToggle={toggleActive}109 onUpdate={updateUser}110 />111 ))}112 </div>113 );114}115116const UserCard = memo(({ user, onToggle, onUpdate }) => {117 console.log(`UserCard ${user.name} rendered`);118 return (119 <div>120 <h3>{user.name}</h3>121 <button onClick={() => onToggle(user.id)}>122 {user.active ? 'Deactivate' : 'Activate'}123 </button>124 <button onClick={() => onUpdate(user.id, { name: user.name + '!' })}>125 Add !126 </button>127 </div>128 );129});130131// 💡 KEY POINTS:132// - useCallback memoizes functions to maintain stable references133// - Essential when passing callbacks to memoized children134// - Use functional updates to avoid stale closures135// - Critical for preventing useEffect infinite loops136//137// ⚠️ DON'T OVERUSE:138// - Not needed for every function139// - Has its own overhead140// - Measure performance impact first