|
| 1 | +import React, { useState, useCallback, memo } from 'react'; |
| 2 | + |
| 3 | +/** |
| 4 | + * CODING EXERCISE 3: useCallback and Unnecessary Re-renders - SOLUTION |
| 5 | + * |
| 6 | + * ANSWER: B) ChildComponent re-renders every time count changes |
| 7 | + * |
| 8 | + * EXPLANATION: |
| 9 | + * |
| 10 | + * 1. FUNCTION REFERENCE PROBLEM: |
| 11 | + * - Every time the parent re-renders, handleClick is recreated |
| 12 | + * - Even though the function does the same thing, it's a NEW reference |
| 13 | + * - React compares props by reference: oldFunction !== newFunction |
| 14 | + * |
| 15 | + * 2. WHY IT RE-RENDERS: |
| 16 | + * - Parent count changes → Parent re-renders |
| 17 | + * - Parent re-render → handleClick recreated with new reference |
| 18 | + * - New reference → Child sees "different" prop → Child re-renders |
| 19 | + * |
| 20 | + * 3. THE SOLUTION: |
| 21 | + * - Use useCallback to memoize the function |
| 22 | + * - Use React.memo to prevent re-renders when props haven't changed |
| 23 | + * - Combine both for optimal performance |
| 24 | + */ |
| 25 | + |
| 26 | +// Problem: Regular child component (re-renders on every parent render) |
| 27 | +const RegularChild = ({ onClick, label }) => { |
| 28 | + console.log(`${label} rendered`); |
| 29 | + return ( |
| 30 | + <div style={{ padding: '10px', backgroundColor: '#ffe6e6', marginTop: '10px', borderRadius: '5px' }}> |
| 31 | + <p><strong>{label}</strong></p> |
| 32 | + <button onClick={onClick}>Click me</button> |
| 33 | + </div> |
| 34 | + ); |
| 35 | +}; |
| 36 | + |
| 37 | +// Solution 1: Memoized child (still re-renders because function reference changes) |
| 38 | +const MemoizedChild = memo(({ onClick, label }) => { |
| 39 | + console.log(`${label} rendered`); |
| 40 | + return ( |
| 41 | + <div style={{ padding: '10px', backgroundColor: '#fff3e6', marginTop: '10px', borderRadius: '5px' }}> |
| 42 | + <p><strong>{label}</strong></p> |
| 43 | + <button onClick={onClick}>Click me</button> |
| 44 | + </div> |
| 45 | + ); |
| 46 | +}); |
| 47 | + |
| 48 | +// Solution 2: Optimized child (won't re-render unnecessarily) |
| 49 | +const OptimizedChild = memo(({ onClick, label }) => { |
| 50 | + console.log(`${label} rendered`); |
| 51 | + return ( |
| 52 | + <div style={{ padding: '10px', backgroundColor: '#e6ffe6', marginTop: '10px', borderRadius: '5px' }}> |
| 53 | + <p><strong>{label}</strong></p> |
| 54 | + <button onClick={onClick}>Click me</button> |
| 55 | + </div> |
| 56 | + ); |
| 57 | +}); |
| 58 | + |
| 59 | +function Solution() { |
| 60 | + const [count, setCount] = useState(0); |
| 61 | + const [clicks1, setClicks1] = useState(0); |
| 62 | + const [clicks2, setClicks2] = useState(0); |
| 63 | + const [clicks3, setClicks3] = useState(0); |
| 64 | + |
| 65 | + // ❌ WRONG: Function recreated on every render |
| 66 | + const handleClick1 = () => { |
| 67 | + setClicks1(prev => prev + 1); |
| 68 | + }; |
| 69 | + |
| 70 | + // ⚠️ PARTIAL: Function recreated on every render (memo doesn't help) |
| 71 | + const handleClick2 = () => { |
| 72 | + setClicks2(prev => prev + 1); |
| 73 | + }; |
| 74 | + |
| 75 | + // ✅ CORRECT: Function memoized with useCallback |
| 76 | + const handleClick3 = useCallback(() => { |
| 77 | + setClicks3(prev => prev + 1); |
| 78 | + }, []); // Empty deps = function never changes |
| 79 | + |
| 80 | + return ( |
| 81 | + <div style={{ padding: '20px', fontFamily: 'Arial' }}> |
| 82 | + <h2>Exercise 3: useCallback & Memoization Solution</h2> |
| 83 | + |
| 84 | + <div style={{ |
| 85 | + marginBottom: '20px', |
| 86 | + padding: '15px', |
| 87 | + backgroundColor: '#e3f2fd', |
| 88 | + borderRadius: '5px' |
| 89 | + }}> |
| 90 | + <h3>Parent State</h3> |
| 91 | + <p>Parent Count: {count}</p> |
| 92 | + <button onClick={() => setCount(count + 1)}> |
| 93 | + Increment Parent Count (Watch Console!) |
| 94 | + </button> |
| 95 | + </div> |
| 96 | + |
| 97 | + {/* Example 1: Regular child without memo */} |
| 98 | + <div style={{ marginBottom: '20px' }}> |
| 99 | + <h3>❌ Problem: Regular Child (No Optimization)</h3> |
| 100 | + <p>Clicks: {clicks1}</p> |
| 101 | + <RegularChild |
| 102 | + onClick={handleClick1} |
| 103 | + label="Regular Child (Always Re-renders)" |
| 104 | + /> |
| 105 | + <pre style={{ |
| 106 | + backgroundColor: '#fff', |
| 107 | + padding: '10px', |
| 108 | + borderRadius: '3px', |
| 109 | + fontSize: '12px', |
| 110 | + overflow: 'auto' |
| 111 | + }}> |
| 112 | +{`const handleClick = () => { |
| 113 | + setClicks(prev => prev + 1); |
| 114 | +}; |
| 115 | +
|
| 116 | +<RegularChild onClick={handleClick} /> |
| 117 | +
|
| 118 | +// Problem: Function recreated every render |
| 119 | +// Child re-renders every time`} |
| 120 | + </pre> |
| 121 | + </div> |
| 122 | + |
| 123 | + {/* Example 2: Memoized child but function still recreated */} |
| 124 | + <div style={{ marginBottom: '20px' }}> |
| 125 | + <h3>⚠️ Partial: Memo Child (Still Re-renders)</h3> |
| 126 | + <p>Clicks: {clicks2}</p> |
| 127 | + <MemoizedChild |
| 128 | + onClick={handleClick2} |
| 129 | + label="Memoized Child (Still Re-renders)" |
| 130 | + /> |
| 131 | + <pre style={{ |
| 132 | + backgroundColor: '#fff', |
| 133 | + padding: '10px', |
| 134 | + borderRadius: '3px', |
| 135 | + fontSize: '12px', |
| 136 | + overflow: 'auto' |
| 137 | + }}> |
| 138 | +{`const MemoizedChild = memo(({ onClick }) => { |
| 139 | + return <button onClick={onClick}>Click</button>; |
| 140 | +}); |
| 141 | +
|
| 142 | +const handleClick = () => { /* ... */ }; |
| 143 | +<MemoizedChild onClick={handleClick} /> |
| 144 | +
|
| 145 | +// Problem: memo() helps, but function still |
| 146 | +// recreated, so props still "change"`} |
| 147 | + </pre> |
| 148 | + </div> |
| 149 | + |
| 150 | + {/* Example 3: Optimized with useCallback + memo */} |
| 151 | + <div style={{ marginBottom: '20px' }}> |
| 152 | + <h3>✅ Solution: useCallback + memo</h3> |
| 153 | + <p>Clicks: {clicks3}</p> |
| 154 | + <OptimizedChild |
| 155 | + onClick={handleClick3} |
| 156 | + label="Optimized Child (No Unnecessary Re-renders)" |
| 157 | + /> |
| 158 | + <pre style={{ |
| 159 | + backgroundColor: '#fff', |
| 160 | + padding: '10px', |
| 161 | + borderRadius: '3px', |
| 162 | + fontSize: '12px', |
| 163 | + overflow: 'auto' |
| 164 | + }}> |
| 165 | +{`const OptimizedChild = memo(({ onClick }) => { |
| 166 | + return <button onClick={onClick}>Click</button>; |
| 167 | +}); |
| 168 | +
|
| 169 | +const handleClick = useCallback(() => { |
| 170 | + setClicks(prev => prev + 1); |
| 171 | +}, []); // Memoized - same reference |
| 172 | +
|
| 173 | +<OptimizedChild onClick={handleClick} /> |
| 174 | +
|
| 175 | +// ✅ Function reference stays the same |
| 176 | +// ✅ memo() prevents re-render |
| 177 | +// ✅ Only re-renders when actually needed`} |
| 178 | + </pre> |
| 179 | + </div> |
| 180 | + |
| 181 | + {/* Key Takeaways */} |
| 182 | + <div style={{ |
| 183 | + padding: '15px', |
| 184 | + backgroundColor: '#f0f0f0', |
| 185 | + borderRadius: '5px', |
| 186 | + marginTop: '20px' |
| 187 | + }}> |
| 188 | + <h3>📚 Key Takeaways</h3> |
| 189 | + <ol> |
| 190 | + <li><strong>Functions are recreated:</strong> On every render, function declarations create new references</li> |
| 191 | + <li><strong>Reference equality:</strong> React compares props using === (reference comparison)</li> |
| 192 | + <li><strong>useCallback:</strong> Memoizes function references between renders</li> |
| 193 | + <li><strong>React.memo:</strong> Prevents re-renders when props haven't changed</li> |
| 194 | + <li><strong>Combine both:</strong> useCallback + memo for optimal performance</li> |
| 195 | + <li><strong>Dependencies matter:</strong> useCallback deps determine when function updates</li> |
| 196 | + </ol> |
| 197 | + </div> |
| 198 | + |
| 199 | + {/* When to Use */} |
| 200 | + <div style={{ |
| 201 | + marginTop: '20px', |
| 202 | + padding: '15px', |
| 203 | + backgroundColor: '#e6f3ff', |
| 204 | + borderRadius: '5px' |
| 205 | + }}> |
| 206 | + <h3>💡 When to Use useCallback</h3> |
| 207 | + <ul> |
| 208 | + <li>✅ Passing callbacks to memoized child components</li> |
| 209 | + <li>✅ Function is a dependency in useEffect/useMemo</li> |
| 210 | + <li>✅ Expensive child components that re-render often</li> |
| 211 | + <li>❌ Simple components without performance issues</li> |
| 212 | + <li>❌ Functions that change on every render anyway</li> |
| 213 | + <li>❌ Premature optimization (measure first!)</li> |
| 214 | + </ul> |
| 215 | + </div> |
| 216 | + |
| 217 | + {/* Common Mistakes */} |
| 218 | + <div style={{ |
| 219 | + marginTop: '20px', |
| 220 | + padding: '15px', |
| 221 | + backgroundColor: '#fff3cd', |
| 222 | + borderRadius: '5px' |
| 223 | + }}> |
| 224 | + <h3>⚠️ Common Mistakes</h3> |
| 225 | + <ul> |
| 226 | + <li>❌ Using useCallback without React.memo on child</li> |
| 227 | + <li>❌ Forgetting dependencies (causes stale closures)</li> |
| 228 | + <li>❌ Over-optimizing everything (adds complexity)</li> |
| 229 | + <li>❌ Using useCallback for functions that change often</li> |
| 230 | + <li>✅ Profile first, optimize when needed</li> |
| 231 | + <li>✅ Use React DevTools Profiler to find bottlenecks</li> |
| 232 | + </ul> |
| 233 | + </div> |
| 234 | + |
| 235 | + {/* Code Comparison */} |
| 236 | + <div style={{ |
| 237 | + marginTop: '20px', |
| 238 | + padding: '15px', |
| 239 | + backgroundColor: '#f8f9fa', |
| 240 | + borderRadius: '5px' |
| 241 | + }}> |
| 242 | + <h3>📊 Performance Comparison</h3> |
| 243 | + <table style={{ width: '100%', borderCollapse: 'collapse' }}> |
| 244 | + <thead> |
| 245 | + <tr style={{ backgroundColor: '#e9ecef' }}> |
| 246 | + <th style={{ padding: '10px', textAlign: 'left', border: '1px solid #dee2e6' }}>Approach</th> |
| 247 | + <th style={{ padding: '10px', textAlign: 'left', border: '1px solid #dee2e6' }}>Re-renders</th> |
| 248 | + <th style={{ padding: '10px', textAlign: 'left', border: '1px solid #dee2e6' }}>Performance</th> |
| 249 | + </tr> |
| 250 | + </thead> |
| 251 | + <tbody> |
| 252 | + <tr> |
| 253 | + <td style={{ padding: '10px', border: '1px solid #dee2e6' }}>Regular Child</td> |
| 254 | + <td style={{ padding: '10px', border: '1px solid #dee2e6' }}>❌ Every parent render</td> |
| 255 | + <td style={{ padding: '10px', border: '1px solid #dee2e6' }}>Poor</td> |
| 256 | + </tr> |
| 257 | + <tr> |
| 258 | + <td style={{ padding: '10px', border: '1px solid #dee2e6' }}>memo() only</td> |
| 259 | + <td style={{ padding: '10px', border: '1px solid #dee2e6' }}>❌ Every parent render</td> |
| 260 | + <td style={{ padding: '10px', border: '1px solid #dee2e6' }}>Poor</td> |
| 261 | + </tr> |
| 262 | + <tr> |
| 263 | + <td style={{ padding: '10px', border: '1px solid #dee2e6' }}>useCallback + memo</td> |
| 264 | + <td style={{ padding: '10px', border: '1px solid #dee2e6' }}>✅ Only when needed</td> |
| 265 | + <td style={{ padding: '10px', border: '1px solid #dee2e6' }}>Excellent</td> |
| 266 | + </tr> |
| 267 | + </tbody> |
| 268 | + </table> |
| 269 | + </div> |
| 270 | + </div> |
| 271 | + ); |
| 272 | +} |
| 273 | + |
| 274 | +export default Solution; |
0 commit comments