How to fix Event listener was added but is preventing re-renders in React
Event listeners added in useEffect without proper dependency handling can trap state values in stale closures, preventing component updates from being recognized and causing UI updates to fail.
Quick fix
Fixes Event listener was added but is preventing re-renders
// WRONG - handler always sees count = 0
useEffect(() => {
window.addEventListener("click", () => {
console.log(count); // Always logs 0
});
}, []); // Empty dependency array
// RIGHT - listener updates when count changes
useEffect(() => {
const handleClick = () => {
console.log(count); // Logs current value
};
window.addEventListener("click", handleClick);
return () => window.removeEventListener("click", handleClick);
}, [count]); // Include count in dependencies- Add state variables to the useEffect dependency array
- Use useRef to access latest state without dependency changes
- Use functional state updates to avoid closure dependency
- Ensure listeners are cleaned up to prevent duplicates
// WRONG - handler always sees count = 0useEffect(() => {window.addEventListener("click", () => {console.log(count); // Always logs 0});}, []); // Empty dependency array// RIGHT - listener updates when count changesuseEffect(() => {const handleClick = () => {console.log(count); // Logs current value};window.addEventListener("click", handleClick);return () => window.removeEventListener("click", handleClick);}, [count]); // Include count in dependenciesEvent listener was added but is preventing re-renders
What this error means
When you attach an event listener (like window.addEventListener or element.addEventListener) in React without proper dependency management, the event handler captures a stale closure of your component's state. This means the handler only has access to state values from when useEffect first ran, not the current values. The event listener itself doesn't prevent re-renders—React still re-renders on state changes—but the listener's frozen state can mask real problems and make updates appear ineffective. If you re-render but the listener still uses old data, parts of your app may seem unresponsive.
Typical symptoms
- Event handlers use outdated state values after component updates
- Clicking buttons or triggering events doesn't seem to affect state
- Console logs inside event handlers show stale values
- UI appears to freeze or not respond to user interactions
- New event listeners are created on every render, accumulating in memory
Common causes
- Event listener added in useEffect with empty dependency array, but handler accesses state variables
- Stale closure: the handler was defined once and never updated, so it always uses initial state values
- Missing cleanup function that removes the old listener before attaching a new one
- Adding multiple listeners without removing previous ones, causing duplicate handlers to fire
- Using state variables directly in addEventListener callback without including them in useEffect dependencies
How to fix "Event listener was added but is preventing re-renders"
If your event handler needs access to state, include that state in the dependency array. This ensures the listener is recreated whenever the state changes.
// WRONG - handler always sees count = 0
useEffect(() => {
window.addEventListener("click", () => {
console.log(count); // Always logs 0
});
}, []); // Empty dependency array
// RIGHT - listener updates when count changes
useEffect(() => {
const handleClick = () => {
console.log(count); // Logs current value
};
window.addEventListener("click", handleClick);
return () => window.removeEventListener("click", handleClick);
}, [count]); // Include count in dependenciesWhen you include dependencies, the old listener is cleaned up and a new one is attached with fresh state values.
If re-creating the listener on every state change causes performance issues, use a useRef to store the current state value. The listener never changes, but always reads fresh data from the ref.
import { useRef } from "react";
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const handleClick = () => {
console.log(countRef.current); // Always current value
};
window.addEventListener("click", handleClick);
return () => window.removeEventListener("click", handleClick);
}, []); // Listener never recreates
}The ref acts as a bridge: it's updated in one effect, read in the handler, with no dependency on state.
Instead of reading state directly in the listener, use the functional form of setState, which React always provides the latest value to.
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = () => {
// setCount receives latest state as argument
setCount(prevCount => {
console.log(prevCount); // Always current
return prevCount + 1;
});
};
window.addEventListener("click", handleClick);
return () => window.removeEventListener("click", handleClick);
}, []); // No dependencies needed
}This works when you only need the current state to compute a new value, not to read arbitrary data.
Always return a cleanup function from useEffect that removes the listener. Without it, each effect run adds a new listener without removing the old one.
useEffect(() => {
const handleScroll = () => {
console.log("scrolling");
};
window.addEventListener("scroll", handleScroll);
// Cleanup function runs before effect re-runs and on unmount
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);The cleanup function prevents memory leaks and duplicate event handlers.
React 19 introduces useEffectEvent, which creates a non-reactive function that always reads the latest state without changing identity.
import { useEffect, useRef } from "react";
// Pseudo-code for useEffectEvent pattern (experimental in React 19)
function Counter() {
const [count, setCount] = useState(0);
// In React 19: useEffectEvent lets you access latest state
// without creating effect dependencies
useEffect(() => {
const handleClick = () => {
// Always accesses current count
console.log(count);
};
window.addEventListener("click", handleClick);
return () => window.removeEventListener("click", handleClick);
}, [count]);
}This removes the closure problem entirely by ensuring handlers always see fresh state.
Advanced notes
Stale Closures Explained: When JavaScript creates a closure (like an event handler function), it captures the variables in its scope. In React, if that closure is defined in useEffect with an empty dependency array, it captures state values from the initial render. Future re-renders don't update the closure—it still references the old values. This is different from inline event handlers on JSX elements, which are re-created every render and always see current state. Performance Consideration: Including every state variable in the dependency array causes the listener to re-attach on every change, which can be expensive for frequent updates. That's why useRef and functional updates are valuable—they let you read current values without recreating the listener. Event Listener Memory Leaks: Not removing listeners in the cleanup function causes severe memory leaks, especially in single-page apps where components mount and unmount repeatedly. Each mount adds listeners without removing old ones, eventually degrading performance. Custom Hooks: The patterns above work well in a custom hook like useEventListener, which handles dependencies and cleanup automatically—reducing boilerplate and bugs.
Frequently asked questions
- What does "Event listener was added but is preventing re-renders" mean?
- addEventListener) in React without proper dependency management, the event handler captures a stale closure of your component's state. This means the handler only has access to state values from when useEffect first ran, not the current values. The event listener itself doesn't prevent re-renders—React still re-renders on state changes—but the listener's frozen state can mask real problems and make updates appear ineffective.
- How do I fix "Event listener was added but is preventing re-renders"?
- Event listeners added in useEffect without proper dependency handling can trap state values in stale closures, preventing component updates from being recognized and causing UI updates to fail.
- What causes "Event listener was added but is preventing re-renders"?
- Event listener added in useEffect with empty dependency array, but handler accesses state variables Stale closure: the handler was defined once and never updated, so it always uses initial state values Missing cleanup function that removes the old listener before attaching a new one
- Is the "Event listener was added but is preventing re-renders" error serious?
- Yes. This is a high-severity error that usually blocks work until it is fixed, but the steps above resolve it.