A Conversation About Memory Leaks Every Junior Dev Should Read
Wed Dec 03 2025

"Bro… the app is again slowing down after 5–6 minutes of running," Rahul whispered like he just broke production.
Sitting across the desk, sipping coffee like a monk who has seen 1000 deploys fail, was our senior engineer, Arvind. He didn't panic. He didn't even blink.
He just asked, "Okay Rahul… show me the code."
Rahul opened his React app.
Arvind stared at the screen. And then he smiled that dangerous senior-dev smile - the one that says "You fool… but don't worry, I shall enlighten you."
"Rahul… you have created a memory leak."
Rahul blinked. "A what?"
"Memory leak," Arvind said. "Think of it like this: your program keeps reserving seats in a movie theater but never frees them. Eventually, no seats left… and your app just dies."
Leak #1: Event Listeners That Never Die
Rahul's Component:
useEffect(() => {
window.addEventListener("resize", () => {
console.log("resized");
});
}, []);
Arvind laughed."Rahul… where is the cleanup?"
Rahul stared."What cleanup?"
Arvind rewrote it:
useEffect(() => {
const handler = () => console.log("resized");
window.addEventListener("resize", handler);
return () => {
window.removeEventListener("resize", handler);
};
}, []);
"See?" Arvind said."Always clean up what you set up. Listeners, intervals, sockets, subscriptions… everything. Otherwise JS will keep them forever like your ex who keeps your hoodie."
Leak #2: setInterval → setINFINITE problem
Rahul had this:
useEffect(() => {
setInterval(() => {
console.log("running...");
}, 1000);
}, []);
"No cleanup??" Arvind shouted dramatically.
Correct version:
useEffect(() => {
const id = setInterval(() => {
console.log("running...");
}, 1000);
return () => clearInterval(id);
}, []);
Leak #3: Storing giant objects in state 'just because'
Rahul's code had:
const [data, setData] = useState(hugeObject);
Arvind looked at him and said, "Bro… did you just store a 30MB JSON response in state? What are you… an S3 bucket?"
Correct approach:
const [items, setItems] = useState(hugeObject.items.slice(0, 50));
Or even better - store only what you need.
Leak #4: Never cancelling API calls
Rahul wrote:
useEffect(() => {
fetch("/api/products")
.then((res) => res.json())
.then(setProducts);
}, []);
Arvind shook his head.
"Imagine the user leaves the page before the fetch finishes. The response still comes… but the component is gone. React gets confused and logs warnings."
Correct version (AbortController):
useEffect(() => {
const controller = new AbortController();
fetch("/api/products", { signal: controller.signal })
.then((res) => res.json())
.then(setProducts)
.catch((err) => {
if (err.name !== "AbortError") console.error(err);
});
return () => controller.abort();
}, []);
Rahul: "So I have to tell the API to chill when I leave?" Arvind: "Exactly. Tell it to stop shouting into the void."
Leak #5: Keeping references alive by accident
Rahul had this debounced function outside the effect:
const debounced = debounce(() => {
console.log("typing...");
}, 500);
useEffect(() => {
inputRef.current.addEventListener("input", debounced);
}, []);
Arvind sighed, "This debounced function will never die. Ever. Because some part of your component still holds on to it."
Correct approach:
useEffect(() => {
const handler = debounce(() => {
console.log("typing...");
}, 500);
const input = inputRef.current;
input.addEventListener("input", handler);
return () => {
input.removeEventListener("input", handler);
handler.cancel();
};
}, []);
"Create functions inside the effect," Arvind explained. "If you create them outside, React doesn't know when to clean them."
Before leaving for lunch, Arvind gave Rahul the golden rule: "Every time you open something, ask yourself: who will close it?"
Listeners? Close them.
Intervals? Clear them.
APIs? Abort them.
Subscriptions? Unsubscribe them.
Coding is like cleaning your room - if you don't do the small cleanups daily, one day you'll find an empty pizza box from 2021 under your chair.
Wed Dec 03 2025

