Skip to content

Commit 8fa265f

Browse files
authored
Merge pull request #378 from codomposer/feat/usecallback
add new coding exercise for usecallback
2 parents df7dc6c + e256641 commit 8fa265f

File tree

4 files changed

+363
-0
lines changed

4 files changed

+363
-0
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { useState, useCallback } from 'react';
2+
3+
/**
4+
* CODING EXERCISE 3: useCallback and Unnecessary Re-renders
5+
*
6+
* PROBLEM:
7+
* In the code below, the ChildComponent re-renders every time the parent's count changes,
8+
* even though the child only depends on the handleClick function.
9+
*
10+
* Options:
11+
* A) ChildComponent re-renders only when button is clicked
12+
* B) ChildComponent re-renders every time count changes
13+
* C) ChildComponent never re-renders
14+
* D) React throws an error about missing dependencies
15+
*
16+
* BONUS: How would you prevent unnecessary re-renders of ChildComponent?
17+
*/
18+
19+
// Child component that should only re-render when its props change
20+
const ChildComponent = ({ onClick }) => {
21+
console.log('ChildComponent rendered');
22+
return (
23+
<div style={{ padding: '10px', backgroundColor: '#e3f2fd', marginTop: '10px', borderRadius: '5px' }}>
24+
<p>I'm a child component</p>
25+
<button onClick={onClick}>Click me from child</button>
26+
</div>
27+
);
28+
};
29+
30+
function Problem() {
31+
const [count, setCount] = useState(0);
32+
const [childClicks, setChildClicks] = useState(0);
33+
34+
// This function is recreated on every render
35+
const handleClick = () => {
36+
setChildClicks(prev => prev + 1);
37+
};
38+
39+
return (
40+
<div style={{ padding: '20px', fontFamily: 'Arial' }}>
41+
<h2>Exercise 3: useCallback & Memoization Problem</h2>
42+
43+
<div style={{ marginBottom: '20px' }}>
44+
<p>Parent Count: {count}</p>
45+
<button onClick={() => setCount(count + 1)}>Increment Parent Count</button>
46+
</div>
47+
48+
<div style={{ marginBottom: '20px' }}>
49+
<p>Child Clicks: {childClicks}</p>
50+
</div>
51+
52+
<ChildComponent onClick={handleClick} />
53+
54+
<div style={{
55+
marginTop: '20px',
56+
padding: '10px',
57+
backgroundColor: '#fff3cd',
58+
border: '1px solid #ffc107',
59+
borderRadius: '5px'
60+
}}>
61+
<p><strong>⚠️ Question:</strong> What happens when you click "Increment Parent Count"?</p>
62+
<p>Think about:</p>
63+
<ul>
64+
<li>Does the ChildComponent re-render?</li>
65+
<li>Why does it re-render (or not)?</li>
66+
<li>How can you prevent unnecessary re-renders?</li>
67+
<li>Check the console to see render logs</li>
68+
</ul>
69+
<p style={{ fontSize: '12px', color: '#856404' }}>
70+
<strong>Tip:</strong> Open the browser console and click the parent button multiple times
71+
</p>
72+
</div>
73+
</div>
74+
);
75+
}
76+
77+
export default Problem;
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
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;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as Problem } from './Problem';
2+
export { default as Solution } from './Solution';

coding-exercise/src/exercises/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as Exercise01 from './exercise-01-state-batching';
22
import * as Exercise02 from './exercise-02-useeffect-dependencies';
3+
import * as Exercise03 from './exercise-03-useCallback-memoization';
34

45
/**
56
* Exercise Registry
@@ -30,6 +31,15 @@ export const exercises = [
3031
Problem: Exercise02.Problem,
3132
Solution: Exercise02.Solution,
3233
},
34+
{
35+
id: 'exercise-03',
36+
title: 'useCallback & Memoization',
37+
description: 'Prevent unnecessary re-renders with useCallback and React.memo',
38+
difficulty: 'Medium',
39+
topics: ['useCallback', 'React.memo', 'Performance', 'Re-renders'],
40+
Problem: Exercise03.Problem,
41+
Solution: Exercise03.Solution,
42+
},
3343
// Add more exercises here...
3444
];
3545

0 commit comments

Comments
 (0)