diff --git a/coding-exercise/src/exercises/exercise-03-useCallback-memoization/Problem.js b/coding-exercise/src/exercises/exercise-03-useCallback-memoization/Problem.js new file mode 100644 index 00000000..b083939b --- /dev/null +++ b/coding-exercise/src/exercises/exercise-03-useCallback-memoization/Problem.js @@ -0,0 +1,77 @@ +import React, { useState, useCallback } from 'react'; + +/** + * CODING EXERCISE 3: useCallback and Unnecessary Re-renders + * + * PROBLEM: + * In the code below, the ChildComponent re-renders every time the parent's count changes, + * even though the child only depends on the handleClick function. + * + * Options: + * A) ChildComponent re-renders only when button is clicked + * B) ChildComponent re-renders every time count changes + * C) ChildComponent never re-renders + * D) React throws an error about missing dependencies + * + * BONUS: How would you prevent unnecessary re-renders of ChildComponent? + */ + +// Child component that should only re-render when its props change +const ChildComponent = ({ onClick }) => { + console.log('ChildComponent rendered'); + return ( +
+

I'm a child component

+ +
+ ); +}; + +function Problem() { + const [count, setCount] = useState(0); + const [childClicks, setChildClicks] = useState(0); + + // This function is recreated on every render + const handleClick = () => { + setChildClicks(prev => prev + 1); + }; + + return ( +
+

Exercise 3: useCallback & Memoization Problem

+ +
+

Parent Count: {count}

+ +
+ +
+

Child Clicks: {childClicks}

+
+ + + +
+

⚠️ Question: What happens when you click "Increment Parent Count"?

+

Think about:

+
    +
  • Does the ChildComponent re-render?
  • +
  • Why does it re-render (or not)?
  • +
  • How can you prevent unnecessary re-renders?
  • +
  • Check the console to see render logs
  • +
+

+ Tip: Open the browser console and click the parent button multiple times +

+
+
+ ); +} + +export default Problem; diff --git a/coding-exercise/src/exercises/exercise-03-useCallback-memoization/Solution.js b/coding-exercise/src/exercises/exercise-03-useCallback-memoization/Solution.js new file mode 100644 index 00000000..ef2ff777 --- /dev/null +++ b/coding-exercise/src/exercises/exercise-03-useCallback-memoization/Solution.js @@ -0,0 +1,274 @@ +import React, { useState, useCallback, memo } from 'react'; + +/** + * CODING EXERCISE 3: useCallback and Unnecessary Re-renders - SOLUTION + * + * ANSWER: B) ChildComponent re-renders every time count changes + * + * EXPLANATION: + * + * 1. FUNCTION REFERENCE PROBLEM: + * - Every time the parent re-renders, handleClick is recreated + * - Even though the function does the same thing, it's a NEW reference + * - React compares props by reference: oldFunction !== newFunction + * + * 2. WHY IT RE-RENDERS: + * - Parent count changes → Parent re-renders + * - Parent re-render → handleClick recreated with new reference + * - New reference → Child sees "different" prop → Child re-renders + * + * 3. THE SOLUTION: + * - Use useCallback to memoize the function + * - Use React.memo to prevent re-renders when props haven't changed + * - Combine both for optimal performance + */ + +// Problem: Regular child component (re-renders on every parent render) +const RegularChild = ({ onClick, label }) => { + console.log(`${label} rendered`); + return ( +
+

{label}

+ +
+ ); +}; + +// Solution 1: Memoized child (still re-renders because function reference changes) +const MemoizedChild = memo(({ onClick, label }) => { + console.log(`${label} rendered`); + return ( +
+

{label}

+ +
+ ); +}); + +// Solution 2: Optimized child (won't re-render unnecessarily) +const OptimizedChild = memo(({ onClick, label }) => { + console.log(`${label} rendered`); + return ( +
+

{label}

+ +
+ ); +}); + +function Solution() { + const [count, setCount] = useState(0); + const [clicks1, setClicks1] = useState(0); + const [clicks2, setClicks2] = useState(0); + const [clicks3, setClicks3] = useState(0); + + // ❌ WRONG: Function recreated on every render + const handleClick1 = () => { + setClicks1(prev => prev + 1); + }; + + // ⚠️ PARTIAL: Function recreated on every render (memo doesn't help) + const handleClick2 = () => { + setClicks2(prev => prev + 1); + }; + + // ✅ CORRECT: Function memoized with useCallback + const handleClick3 = useCallback(() => { + setClicks3(prev => prev + 1); + }, []); // Empty deps = function never changes + + return ( +
+

Exercise 3: useCallback & Memoization Solution

+ +
+

Parent State

+

Parent Count: {count}

+ +
+ + {/* Example 1: Regular child without memo */} +
+

❌ Problem: Regular Child (No Optimization)

+

Clicks: {clicks1}

+ +
+{`const handleClick = () => {
+  setClicks(prev => prev + 1);
+};
+
+
+
+// Problem: Function recreated every render
+// Child re-renders every time`}
+        
+
+ + {/* Example 2: Memoized child but function still recreated */} +
+

⚠️ Partial: Memo Child (Still Re-renders)

+

Clicks: {clicks2}

+ +
+{`const MemoizedChild = memo(({ onClick }) => {
+  return ;
+});
+
+const handleClick = () => { /* ... */ };
+
+
+// Problem: memo() helps, but function still
+// recreated, so props still "change"`}
+        
+
+ + {/* Example 3: Optimized with useCallback + memo */} +
+

✅ Solution: useCallback + memo

+

Clicks: {clicks3}

+ +
+{`const OptimizedChild = memo(({ onClick }) => {
+  return ;
+});
+
+const handleClick = useCallback(() => {
+  setClicks(prev => prev + 1);
+}, []); // Memoized - same reference
+
+
+
+// ✅ Function reference stays the same
+// ✅ memo() prevents re-render
+// ✅ Only re-renders when actually needed`}
+        
+
+ + {/* Key Takeaways */} +
+

📚 Key Takeaways

+
    +
  1. Functions are recreated: On every render, function declarations create new references
  2. +
  3. Reference equality: React compares props using === (reference comparison)
  4. +
  5. useCallback: Memoizes function references between renders
  6. +
  7. React.memo: Prevents re-renders when props haven't changed
  8. +
  9. Combine both: useCallback + memo for optimal performance
  10. +
  11. Dependencies matter: useCallback deps determine when function updates
  12. +
+
+ + {/* When to Use */} +
+

💡 When to Use useCallback

+ +
+ + {/* Common Mistakes */} +
+

⚠️ Common Mistakes

+ +
+ + {/* Code Comparison */} +
+

📊 Performance Comparison

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ApproachRe-rendersPerformance
Regular Child❌ Every parent renderPoor
memo() only❌ Every parent renderPoor
useCallback + memo✅ Only when neededExcellent
+
+
+ ); +} + +export default Solution; diff --git a/coding-exercise/src/exercises/exercise-03-useCallback-memoization/index.js b/coding-exercise/src/exercises/exercise-03-useCallback-memoization/index.js new file mode 100644 index 00000000..e2565cae --- /dev/null +++ b/coding-exercise/src/exercises/exercise-03-useCallback-memoization/index.js @@ -0,0 +1,2 @@ +export { default as Problem } from './Problem'; +export { default as Solution } from './Solution'; diff --git a/coding-exercise/src/exercises/index.js b/coding-exercise/src/exercises/index.js index b663d414..f7518453 100644 --- a/coding-exercise/src/exercises/index.js +++ b/coding-exercise/src/exercises/index.js @@ -1,5 +1,6 @@ import * as Exercise01 from './exercise-01-state-batching'; import * as Exercise02 from './exercise-02-useeffect-dependencies'; +import * as Exercise03 from './exercise-03-useCallback-memoization'; /** * Exercise Registry @@ -30,6 +31,15 @@ export const exercises = [ Problem: Exercise02.Problem, Solution: Exercise02.Solution, }, + { + id: 'exercise-03', + title: 'useCallback & Memoization', + description: 'Prevent unnecessary re-renders with useCallback and React.memo', + difficulty: 'Medium', + topics: ['useCallback', 'React.memo', 'Performance', 'Re-renders'], + Problem: Exercise03.Problem, + Solution: Exercise03.Solution, + }, // Add more exercises here... ];