Lovable-generated useEffect hooks often trigger unexpected behavior because of missing cleanup functions, incorrect dependency arrays, and side effects running on every render. Fix this by always returning a cleanup function, including every referenced variable in the dependency array, and using empty arrays only for effects that should run once on mount. Never suppress the exhaustive-deps ESLint warning — fix the dependency issue instead.
Why useEffect triggers unexpected behavior in Lovable-generated code
useEffect is the React hook for running side effects — things like fetching data, setting up subscriptions, or updating the document title. Lovable's AI generates useEffect hooks frequently, but sometimes with subtle bugs that cause infinite loops, memory leaks, or stale data. The most common issue is missing cleanup. When a useEffect sets up a timer or subscription, it must return a cleanup function that tears it down. Without cleanup, timers keep running after the component unmounts, subscriptions accumulate, and event listeners pile up. The second most common issue is the dependency array. If the array is missing or incomplete, the effect either runs on every render (causing performance problems) or never re-runs when it should (causing stale data). The ESLint exhaustive-deps rule warns about missing dependencies for exactly this reason.
- useEffect has no cleanup function — timers and subscriptions leak when the component unmounts
- Dependency array is empty but the effect reads state or props that change over time
- Dependency array is missing entirely, causing the effect to run on every single render
- Effect updates a state variable that is also in its dependency array, creating an infinite loop
- Side effects that should run once (API calls) run repeatedly because dependencies change on every render
Error messages you might see
React Hook useEffect has a missing dependency: 'fetchData'The effect calls fetchData but it is not in the dependency array. Either add it to the array or move the function inside the effect. Do not suppress this warning.
Warning: Can't perform a React state update on an unmounted componentAn async operation (fetch or timer) inside useEffect completes after the component unmounts and tries to update state. Add a cleanup flag: let cancelled = false, and check it before setting state.
Maximum update depth exceededThe effect updates state that is in its dependency array, triggering the effect again infinitely. Use functional setState or restructure to break the cycle.
Before you start
- A Lovable project with useEffect hooks that behave unexpectedly
- Access to the browser console to see warnings and errors related to effects
- Basic understanding of what useEffect does (runs code after the component renders)
How to fix it
Always add cleanup functions to useEffect
Cleanup prevents memory leaks by tearing down timers, subscriptions, and event listeners when the component unmounts
Always add cleanup functions to useEffect
Cleanup prevents memory leaks by tearing down timers, subscriptions, and event listeners when the component unmounts
Every useEffect that creates something persistent (timer, subscription, event listener) must return a function that cleans it up. This return function runs when the component unmounts or before the effect re-runs. Without it, the old timer or subscription keeps running alongside the new one, causing duplicate behavior and memory leaks.
useEffect(() => { const interval = setInterval(() => { console.log("tick"); }, 1000); // No cleanup — interval runs forever}, []);useEffect(() => { const interval = setInterval(() => { console.log("tick"); }, 1000); // Cleanup: clear the interval when component unmounts return () => clearInterval(interval);}, []);Expected result: The interval runs while the component is mounted and stops cleanly when it unmounts. No memory leak.
Fix dependency arrays to include all referenced variables
Missing dependencies cause effects to use stale values from the initial render instead of current values
Fix dependency arrays to include all referenced variables
Missing dependencies cause effects to use stale values from the initial render instead of current values
Review the dependency array of each useEffect. Every state variable, prop, or function that the effect reads must be included in the array. If the ESLint exhaustive-deps rule warns about a missing dependency, add it. If adding it causes the effect to run too often, restructure: move the function inside the effect, use functional setState, or move the value into a useRef.
const [userId, setUserId] = useState("1");useEffect(() => { // Bug: fetches user 1 forever, even after userId changes fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);}, []); // Missing userId in dependency arrayconst [userId, setUserId] = useState("1");useEffect(() => { let cancelled = false; fetch(`/api/users/${userId}`) .then(r => r.json()) .then(data => { if (!cancelled) setUser(data); }); // Cleanup: prevent stale fetch from updating state return () => { cancelled = true; };}, [userId]); // userId in dependency array — re-fetches when it changesExpected result: The effect re-fetches user data whenever userId changes. No stale data from the initial user.
Prevent async state updates on unmounted components
Setting state after unmount causes a React warning and can lead to memory leaks
Prevent async state updates on unmounted components
Setting state after unmount causes a React warning and can lead to memory leaks
When a useEffect contains an async operation (fetch, setTimeout), the component may unmount before the operation completes. If the callback then calls setState, React warns about updating an unmounted component. Fix this by using a cancelled flag in the cleanup function. Check the flag before calling setState.
useEffect(() => { fetch("/api/data") .then(r => r.json()) .then(data => setData(data)); // May run after unmount}, []);useEffect(() => { let cancelled = false; fetch("/api/data") .then(r => r.json()) .then(data => { // Only update state if component is still mounted if (!cancelled) { setData(data); } }); return () => { cancelled = true; };}, []);Expected result: No React warning about updating unmounted components. State is only set when the component is still active.
Break infinite loops caused by effects updating their own dependencies
An effect that updates state included in its dependency array triggers itself infinitely
Break infinite loops caused by effects updating their own dependencies
An effect that updates state included in its dependency array triggers itself infinitely
If your effect needs to update a value that is also in its dependency array, use functional setState to avoid reading the state variable directly. If restructuring involves multiple effects and complex interdependencies, RapidDev's engineers have debugged useEffect chains across 600+ Lovable projects and can resolve the issue.
const [count, setCount] = useState(0);// Infinite loop: effect reads count, updates count,// which changes count, which triggers the effect againuseEffect(() => { setCount(count + 1);}, [count]);const [count, setCount] = useState(0);// Fixed: functional update does not read count from the closure// Effect runs once on mount and sets count to 1useEffect(() => { setCount((prev) => prev + 1);}, []); // No dependency on count needed with functional updateExpected result: The effect runs once and increments count without triggering an infinite loop.
Complete code example
1import { useState, useEffect } from "react";23type FetchState<T> = {4 data: T | null;5 loading: boolean;6 error: string | null;7};89// Custom hook with proper cleanup and dependency handling10export function useFetchData<T>(url: string): FetchState<T> {11 const [state, setState] = useState<FetchState<T>>({12 data: null,13 loading: true,14 error: null,15 });1617 useEffect(() => {18 // Cleanup flag prevents state updates after unmount19 let cancelled = false;2021 setState({ data: null, loading: true, error: null });2223 fetch(url)24 .then((response) => {25 if (!response.ok) {26 throw new Error(`HTTP ${response.status}: ${response.statusText}`);27 }28 return response.json();29 })30 .then((data: T) => {31 if (!cancelled) {32 setState({ data, loading: false, error: null });33 }34 })35 .catch((error) => {36 if (!cancelled) {37 setState({ data: null, loading: false, error: error.message });38 }39 });4041 // Cleanup: mark as cancelled so stale responses are ignored42 return () => {43 cancelled = true;44 };45 }, [url]); // Re-fetches when URL changes4647 return state;48}Best practices to prevent this
- Always return a cleanup function from useEffect when it creates timers, subscriptions, or event listeners
- Include every state variable, prop, and function read inside the effect in the dependency array
- Use an empty dependency array [] only for effects that should run once on mount and never again
- Never suppress the exhaustive-deps ESLint warning — fix the underlying dependency issue instead
- Use a cancelled flag in async effects to prevent state updates on unmounted components
- Use functional setState inside effects to avoid needing the state variable in the dependency array
- Split complex effects into multiple smaller effects, each with its own focused dependency array
- Do not put object or array literals in dependency arrays — they create new references on every render
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
My Lovable project has useEffect issues. Here is the component with the problematic effect: [paste your component code here] Please: 1. Identify all useEffect bugs (missing cleanup, wrong dependencies, infinite loops) 2. Fix each effect with proper cleanup functions and dependency arrays 3. Add cancelled flags for async operations 4. Split any overly complex effects into smaller, focused effects 5. Explain which dependencies are needed and why
Fix the useEffect hooks in @src/pages/Dashboard.tsx. Add cleanup functions to every effect that creates a timer or subscription. Fix the dependency arrays to include all referenced state variables. Add a cancelled flag to the data fetching effect to prevent state updates after unmount. If the ESLint exhaustive-deps rule warns about missing dependencies, fix them by adding the dependency or restructuring the code.
Frequently asked questions
Why does my useEffect run on every render in Lovable?
If the dependency array is missing entirely, useEffect runs after every render. If the array contains objects or arrays created during render, they are new references each time, triggering the effect. Use an empty array for mount-only effects, or memoize objects with useMemo.
What should I put in the useEffect dependency array?
Every state variable, prop, and function that the effect reads. If you reference userId inside the effect, include [userId] in the array. If the effect should only run once on mount, use an empty array [] — but only if the effect truly does not depend on any changing values.
How do I prevent memory leaks from useEffect?
Return a cleanup function that clears timers (clearInterval), removes event listeners, and unsubscribes from subscriptions. For async operations, use a cancelled flag to prevent state updates after the component unmounts.
How do I stop an infinite loop in useEffect?
An infinite loop happens when the effect updates a state variable that is in its dependency array. Use functional setState (prev => prev + 1) to remove the variable from the dependency array, or move the state to a useRef if the effect needs to read but not trigger on it.
Should I suppress the exhaustive-deps ESLint warning?
Almost never. The warning exists to prevent stale data and infinite loop bugs. If adding a dependency causes problems, restructure the code: move functions inside the effect, use functional setState, or split into multiple effects.
What if I can't fix this myself?
If your component has multiple interacting useEffect hooks causing cascading re-renders or stale data, RapidDev's engineers can untangle and fix the effect chains. They have debugged useEffect issues across 600+ Lovable projects.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your issue.
Book a free consultation