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:

  1. Provider: A component that "provides" the data to its children
  2. 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 Example
2import { createContext, useContext, useState, useEffect } from 'react';
3
4// Step 1: Create Context
5const ThemeContext = createContext({
6 theme: 'light',
7 toggleTheme: () => {},
8 colors: {}
9});
10
11// Step 2: Provider Component
12function ThemeProvider({ children }) {
13 const [theme, setTheme] = useState(() =>
14 localStorage.getItem('theme') || 'light'
15 );
16
17 const themes = {
18 light: { bg: '#fff', text: '#333', primary: '#007bff' },
19 dark: { bg: '#212529', text: '#fff', primary: '#0d6efd' }
20 };
21
22 const toggleTheme = () => {
23 const newTheme = theme === 'light' ? 'dark' : 'light';
24 setTheme(newTheme);
25 localStorage.setItem('theme', newTheme);
26 };
27
28 return (
29 <ThemeContext.Provider value={{
30 theme,
31 toggleTheme,
32 colors: themes[theme]
33 }}>
34 {children}
35 </ThemeContext.Provider>
36 );
37}
38
39// Step 3: Custom Hook
40function useTheme() {
41 const context = useContext(ThemeContext);
42 if (!context) {
43 throw new Error('useTheme must be used within ThemeProvider');
44 }
45 return context;
46}
47
48// Step 4: Using the Context
49function Header() {
50 const { theme, toggleTheme, colors } = useTheme();
51
52 return (
53 <header style={{ backgroundColor: colors.bg, color: colors.text }}>
54 <h1>My App</h1>
55 <button onClick={toggleTheme}>
56 {theme === 'light' ? '🌙' : '☀️'} Toggle Theme
57 </button>
58 </header>
59 );
60}
61
62function Card({ title, children }) {
63 const { colors } = useTheme();
64
65 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}
76
77// Main App
78function 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}
90
91// 💡 KEY POINTS:
92// - Context provides global state without prop drilling
93// - Always use custom hooks for better error handling
94// - Split contexts for better performance
95// - 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

  1. State: An object containing all your component's data
  2. Action: An object describing what happened (usually has a 'type' property)
  3. Reducer: A pure function that takes the current state and an action, then returns the new state
  4. 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 Example
2import { useReducer, createContext, useContext } from 'react';
3
4// Action types
5const ACTIONS = {
6 ADD_ITEM: 'ADD_ITEM',
7 REMOVE_ITEM: 'REMOVE_ITEM',
8 UPDATE_QTY: 'UPDATE_QTY',
9 CLEAR_CART: 'CLEAR_CART'
10};
11
12// Initial state
13const initialState = {
14 items: [],
15 total: 0
16};
17
18// Reducer function - all state logic in one place
19function cartReducer(state, action) {
20 switch (action.type) {
21 case ACTIONS.ADD_ITEM: {
22 const existing = state.items.find(item => item.id === action.payload.id);
23
24 if (existing) {
25 return {
26 ...state,
27 items: state.items.map(item =>
28 item.id === action.payload.id
29 ? { ...item, quantity: item.quantity + 1 }
30 : item
31 )
32 };
33 }
34
35 return {
36 ...state,
37 items: [...state.items, { ...action.payload, quantity: 1 }]
38 };
39 }
40
41 case ACTIONS.REMOVE_ITEM:
42 return {
43 ...state,
44 items: state.items.filter(item => item.id !== action.payload)
45 };
46
47 case ACTIONS.UPDATE_QTY:
48 return {
49 ...state,
50 items: state.items.map(item =>
51 item.id === action.payload.id
52 ? { ...item, quantity: action.payload.quantity }
53 : item
54 ).filter(item => item.quantity > 0)
55 };
56
57 case ACTIONS.CLEAR_CART:
58 return initialState;
59
60 default:
61 return state;
62 }
63}
64
65// Cart Context
66const CartContext = createContext(null);
67
68function CartProvider({ children }) {
69 const [state, dispatch] = useReducer(cartReducer, initialState);
70
71 // Calculate total
72 const total = state.items.reduce(
73 (sum, item) => sum + item.price * item.quantity,
74 0
75 );
76
77 // Action creators
78 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 });
83
84 return (
85 <CartContext.Provider value={{
86 items: state.items,
87 total,
88 addItem,
89 removeItem,
90 updateQuantity,
91 clearCart
92 }}>
93 {children}
94 </CartContext.Provider>
95 );
96}
97
98// Custom hook
99function useCart() {
100 const context = useContext(CartContext);
101 if (!context) throw new Error('useCart must be used within CartProvider');
102 return context;
103}
104
105// Usage Example
106function ProductCard({ product }) {
107 const { addItem } = useCart();
108
109 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}
117
118function Cart() {
119 const { items, total, updateQuantity, removeItem, clearCart } = useCart();
120
121 if (items.length === 0) return <p>Cart is empty</p>;
122
123 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}
140
141// 💡 KEY BENEFITS:
142// - Centralized state logic
143// - Predictable updates
144// - Easy to test
145// - 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:

  1. A function that returns the calculated value
  2. 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

  1. Forgetting dependencies: Always include all variables used in the calculation
  2. Over-memoizing: Not everything needs to be memoized
  3. Memoizing primitives: Usually unnecessary for strings, numbers, booleans
  4. Breaking referential equality: Creating new functions/objects in the dependency array
1// 🚀 useMemo: Performance Optimization Examples
2import { useState, useMemo, memo } from 'react';
3
4// Example 1: Expensive Calculation
5function PrimeCalculator() {
6 const [count, setCount] = useState(1000);
7 const [dark, setDark] = useState(false);
8
9 // ✅ Only recalculates when count changes
10 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]);
18
19 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}
27
28function 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}
34
35// Example 2: Filtering Large Lists
36function ProductList() {
37 const [filter, setFilter] = useState('');
38 const [sort, setSort] = useState('name');
39
40 // Generate products once
41 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 );
49
50 // Filter and sort products
51 const displayProducts = useMemo(() => {
52 console.log('Filtering products...');
53 return products
54 .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]);
61
62 return (
63 <div>
64 <input
65 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}
81
82// Example 3: Stable Object References
83function Parent() {
84 const [count, setCount] = useState(0);
85 const [name, setName] = useState('');
86
87 // ✅ Object reference stays stable unless name changes
88 const config = useMemo(() => ({
89 theme: 'dark',
90 user: name,
91 features: ['dashboard', 'analytics']
92 }), [name]);
93
94 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}
102
103const ExpensiveChild = memo(({ config }) => {
104 console.log('ExpensiveChild rendered');
105 return <div>Theme: {config.theme}, User: {config.user}</div>;
106});
107
108// 💡 WHEN TO USE useMemo:
109// - Expensive calculations (sorting, filtering, computing)
110// - Creating objects/arrays passed to memoized children
111// - Complex data transformations
112//
113// ⚠️ DON'T USE FOR:
114// - Simple calculations
115// - Primitive values
116// - 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

  1. Stable event handlers: Keep function references stable across renders
  2. Callback with state updates: Use functional updates to avoid stale closures
  3. Complex event handling: Memoize handlers that perform multiple operations
  4. API calls and effects: Prevent infinite loops in useEffect
1// 🎯 useCallback: Function Optimization Examples
2import { useState, useCallback, useEffect, memo } from 'react';
3
4// Example 1: Basic Function Memoization
5function TodoApp() {
6 const [count, setCount] = useState(0);
7 const [todos, setTodos] = useState([]);
8
9 // ✅ Stable function reference
10 const addTodo = useCallback(() => {
11 setTodos(prev => [...prev, {
12 id: Date.now(),
13 text: `Todo ${prev.length + 1}`
14 }]);
15 }, []); // Empty deps = never recreated
16
17 const removeTodo = useCallback((id) => {
18 setTodos(prev => prev.filter(todo => todo.id !== id));
19 }, []); // Functional update = no deps needed
20
21 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}
28
29// Memoized child - only re-renders when props change
30const 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});
44
45// Example 2: useCallback with useEffect
46function SearchBox() {
47 const [query, setQuery] = useState('');
48 const [results, setResults] = useState([]);
49
50 // ✅ Stable search function for useEffect
51 const search = useCallback(async (searchQuery) => {
52 if (!searchQuery) {
53 setResults([]);
54 return;
55 }
56
57 console.log(`Searching for: ${searchQuery}`);
58 // Simulate API call
59 await new Promise(r => setTimeout(r, 300));
60 setResults([`Result for ${searchQuery}`, `Another ${searchQuery} result`]);
61 }, []);
62
63 // Debounced search
64 useEffect(() => {
65 const timer = setTimeout(() => search(query), 500);
66 return () => clearTimeout(timer);
67 }, [query, search]); // search is stable!
68
69 return (
70 <div>
71 <input
72 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}
80
81// Example 3: Complex Event Handlers
82function UserList() {
83 const [users, setUsers] = useState([
84 { id: 1, name: 'Alice', active: true },
85 { id: 2, name: 'Bob', active: false }
86 ]);
87
88 // ✅ Generic update function
89 const updateUser = useCallback((id, updates) => {
90 setUsers(prev => prev.map(user =>
91 user.id === id ? { ...user, ...updates } : user
92 ));
93 }, []);
94
95 // ✅ Specific action handlers
96 const toggleActive = useCallback((id) => {
97 setUsers(prev => prev.map(user =>
98 user.id === id ? { ...user, active: !user.active } : user
99 ));
100 }, []);
101
102 return (
103 <div>
104 {users.map(user => (
105 <UserCard
106 key={user.id}
107 user={user}
108 onToggle={toggleActive}
109 onUpdate={updateUser}
110 />
111 ))}
112 </div>
113 );
114}
115
116const 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});
130
131// 💡 KEY POINTS:
132// - useCallback memoizes functions to maintain stable references
133// - Essential when passing callbacks to memoized children
134// - Use functional updates to avoid stale closures
135// - Critical for preventing useEffect infinite loops
136//
137// ⚠️ DON'T OVERUSE:
138// - Not needed for every function
139// - Has its own overhead
140// - Measure performance impact first