Stale state happens when a closure captures an outdated value. Fix it by using functional updates (setState(prev => prev + 1)) instead of referencing state directly, and always include state variables in your useEffect dependency arrays.
Why stale state happens in Lovable hooks
JavaScript closures capture variables at the time they are created. When you use state inside a useEffect or event handler without listing it as a dependency, the callback holds a reference to the old value — not the current one. This is the #1 source of subtle bugs in Lovable projects that use React hooks.
- Missing dependencies in useEffect dependency arrays
- Using state directly in setTimeout or setInterval callbacks
- Event handlers defined outside the render cycle that close over stale values
- Passing stale callbacks as props to memoized child components
- Reading state inside async functions after await points
Error messages you might see
Warning: Maximum update depth exceededThis usually means a useEffect triggers a setState that re-runs the effect infinitely. Check your dependency array — you may be creating a new object or array reference on every render that triggers the effect again.
TypeError: Cannot read properties of undefined (reading 'map')Often caused by stale state where an array hasn't been populated yet. The component captures the initial empty/undefined state and tries to map over it. Use optional chaining (data?.map) or ensure state is initialized as an empty array.
React Hook useEffect has a missing dependency: 'count'The exhaustive-deps ESLint rule is warning you that your effect reads 'count' but doesn't include it in the dependency array. Either add it as a dependency or switch to a functional update (setCount(prev => prev + 1)) so the effect doesn't need to read the current value.
State update on an unmounted componentAn async operation completed after the component unmounted and tried to call setState. This is often related to stale closures in cleanup functions. Use an abort controller or a ref flag to skip the update if the component has unmounted.
Before you start
- Your Lovable project is open and running in development mode
- You can access the component file that has the stale state issue
- Basic familiarity with React hooks (useState, useEffect)
How to fix it
Use functional state updates
Ensures you always reference the latest state value, not a stale closure
Use functional state updates
Ensures you always reference the latest state value, not a stale closure
Instead of referencing state directly in your updater, pass a function to setState. React will call it with the current value, so closures can never go stale.
// Stale: captures count at render timeconst handleClick = () => { setCount(count + 1);};// In an interval — always adds 1 to// the INITIAL count valueuseEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id);}, []);// Fixed: always uses latest stateconst handleClick = () => { setCount(prev => prev + 1);};// In an interval — correctly increments// from whatever the current value isuseEffect(() => { const id = setInterval(() => { setCount(prev => prev + 1); }, 1000); return () => clearInterval(id);}, []);Expected result: State updates correctly even inside intervals, timeouts, and async callbacks.
Add all dependencies to useEffect
Prevents the effect from closing over outdated values
Add all dependencies to useEffect
Prevents the effect from closing over outdated values
Every variable from the component scope that your effect reads must appear in the dependency array. If you can't add a dependency without causing infinite loops, extract the logic into a function or use a ref.
// Missing dependency — effect never// re-runs when 'query' changesuseEffect(() => { fetchResults(query).then(setResults);}, []);// ESLint warns: missing dependency 'query'// Correct: re-fetches when query changesuseEffect(() => { let cancelled = false; fetchResults(query).then(data => { if (!cancelled) setResults(data); }); return () => { cancelled = true; };}, [query]);Expected result: The effect re-runs whenever query changes and always uses the latest value. Stale responses are safely ignored.
Use useRef for read-only access without re-renders
Gives you a mutable container that always holds the latest value
Use useRef for read-only access without re-renders
Gives you a mutable container that always holds the latest value
When you need to read the current value inside a callback but don't want that callback to re-run when the value changes (like logging or analytics), store the value in a ref.
// Stale: logs initial count foreveruseEffect(() => { const id = setInterval(() => { console.log('Count:', count); }, 5000); return () => clearInterval(id);}, []);const countRef = useRef(count);countRef.current = count; // sync on every renderuseEffect(() => { const id = setInterval(() => { console.log('Count:', countRef.current); }, 5000); return () => clearInterval(id);}, []); // no dependency neededExpected result: The interval always reads the current count without needing to restart.
Complete code example
1import { useState, useCallback, useRef, useEffect } from "react";23export function useCounter(initial = 0) {4 const [count, setCount] = useState(initial);5 const countRef = useRef(count);67 // Keep ref in sync for external reads8 useEffect(() => {9 countRef.current = count;10 }, [count]);1112 const increment = useCallback(() => {13 setCount(prev => prev + 1);14 }, []);1516 const decrement = useCallback(() => {17 setCount(prev => prev - 1);18 }, []);1920 const reset = useCallback(() => {21 setCount(initial);22 }, [initial]);2324 // Safe to use in intervals / async code25 const getCount = useCallback(() => countRef.current, []);2627 return { count, increment, decrement, reset, getCount };28}Best practices to prevent this
- Always use functional updates when new state depends on previous state
- Enable the react-hooks/exhaustive-deps ESLint rule and treat warnings as errors
- Keep effects focused — one effect per concern, not a mega-effect
- Wrap handlers in useCallback when passing them to memoized children
- Prefer useReducer for complex state logic — reducers never have stale closures
- Use abort controllers in async effects to prevent state updates after unmount
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a React app in Lovable and I'm getting stale state in my hooks. My component has a useEffect that reads `count` but it always shows the initial value of 0 even after clicking increment. Here's my code: [paste your component code here] Please: 1. Explain exactly why the state is stale (which closure is capturing the old value) 2. Show me the corrected code using functional updates 3. Explain when I should use useRef vs functional updates 4. Show me how to add proper cleanup to prevent memory leaks
Fix the stale state issue in this component. Specifically: - Replace all direct state references in setters with functional updates (prev => ...) - Ensure all useEffect dependency arrays include every referenced variable - Add cleanup functions to effects with subscriptions or timers - Wrap callbacks passed to children in useCallback
Frequently asked questions
What is stale state in React?
Stale state occurs when a closure (like a callback or effect) captures an old value of state instead of the current one. This happens because JavaScript closures capture variables by value at the time they are created. When state updates, existing closures still hold the old value.
Why does my useEffect always show the initial state value?
Your effect likely has an empty dependency array [] but references state directly. The effect closure captures the initial state value and never updates. Use functional updates (setState(prev => ...)) or add the state variable to the dependency array so the effect re-runs with the new value.
Should I use useRef or functional updates to fix stale state?
Use functional updates (setState(prev => prev + 1)) when you need to update state based on its previous value — this is the most common fix. Use useRef when you need to read the latest value without triggering a re-render, such as in logging, analytics, or event handlers that shouldn't cause the component to update.
Can stale state cause memory leaks in Lovable?
Not directly, but the patterns that cause stale state (missing cleanup in useEffect, uncancelled async operations) can cause memory leaks. Always return a cleanup function from effects that create subscriptions, timers, or async operations.
How do I debug stale state issues?
Add console.log statements inside your effect or callback to see what value is captured. Compare it to the value shown in React DevTools. If they differ, you have a stale closure. The react-hooks/exhaustive-deps ESLint rule catches most cases automatically.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your issue.
Book a free consultation