-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathstale-closures-in-useEffect.js
More file actions
114 lines (99 loc) · 3.21 KB
/
stale-closures-in-useEffect.js
File metadata and controls
114 lines (99 loc) · 3.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
* 🧠 Concept: Stale Closures in useEffect
*
* ------------------------------------------------------------
* 🧾 Why This Matters?
* - React hooks heavily rely on closures
* - Stale closures are a common source of bugs in useEffect
* - Understanding this helps write more predictable effects
*
* 💡 Key Insight: Effects "close over" the values from their render
*/
import { useState, useEffect } from "react";
/**
* 🛠 Demonstration Component: Counter with Stale Closure Issue
*/
function StaleCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
// 🚨 Problem: This will always use the initial `count` value
// because it's closed over in the initial effect
setCount(count + 1);
}, 1000);
return () => clearInterval(intervalId);
}, []); // Empty dependency array means this runs once
return (
<div style={{ padding: "1rem", fontFamily: "sans-serif" }}>
<h2>Stale Closure Example</h2>
<p>Counter: {count}</p>
<p style={{ color: "red" }}>
❌ Bug: Counter will only update to 1 and stop
</p>
</div>
);
}
/**
* 🛠 Solution Component: Correct Dependency Handling
*/
function FixedCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
// ✅ Solution 1: Use functional update to avoid stale closure
setCount((prevCount) => prevCount + 1);
}, 1000);
return () => clearInterval(intervalId);
}, []); // Empty deps okay here because we're using functional update
return (
<div style={{ padding: "1rem", fontFamily: "sans-serif" }}>
<h2>Fixed Version</h2>
<p>Counter: {count}</p>
<p style={{ color: "green" }}>
✅ Fixed: Counter increments correctly using functional update
</p>
</div>
);
}
/**
* 🛠 Alternative Solution: Proper Dependency Array
*/
function DependencyCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(intervalId);
}, [count]); // ✅ Solution 2: Include count in dependencies
return (
<div style={{ padding: "1rem", fontFamily: "sans-serif" }}>
<h2>Dependency Array Solution</h2>
<p>Counter: {count}</p>
<p style={{ color: "green" }}>
✅ Fixed: Counter increments correctly with proper dependencies
</p>
<p style={{ color: "orange" }}>
⚠️ Note: This causes the effect to re-run on every count change,
which may not be optimal for intervals
</p>
</div>
);
}
/**
* 🧵 Summary:
*
* 💥 Stale Closure Problem:
* - Effects capture (close over) values when they're created
* - Without proper dependencies, they don't "see" updates
*
* 🔧 Solutions:
* 1. Use functional updates (for state setters)
* 2. Include all dependencies in the dependency array
* 3. For callbacks, consider useCallback to avoid recreating functions
*
* � When to Use Which:
* - Functional updates: When you only need previous state
* - Dependency array: When you need current props/state in effect
*/
export { StaleCounter, FixedCounter, DependencyCounter };