If you have ever launched a Rork-generated app only to watch your console flood with repeated log entries and the UI grind to a halt, you have hit a useEffect infinite loop. The code looks structurally correct, yet something keeps triggering a re-render cycle that never stops.
AI-generated code is particularly prone to this problem. The patterns are subtle — a dependency array that includes the value being updated, an object that gets recreated on every render, a callback function passed as a prop that never stabilises. Once you know the five root causes, debugging becomes quick and methodical.
What an Infinite Loop Actually Looks Like
React Native runs useEffect after every render where one of the values in the dependency array has changed. When the effect itself triggers a state update that is also in the dependency array, you get a self-reinforcing cycle:
- Component renders
useEffectrunssetStateis called inside the effect- Component re-renders due to state change
- Dependency array value has changed — effect runs again
- Back to step 3 (infinite)
This degrades performance, drains battery, and can crash the app entirely on lower-end devices. Here are the five patterns that cause it in Rork projects.
The 5 Patterns That Cause Infinite Loops
Pattern 1: State That Is Updated Inside the Effect Is Also a Dependency
This is the most common mistake in Rork-generated code.
// Bad — setting data triggers a re-render, which triggers the effect again
const [data, setData] = useState([]);
useEffect(() => {
fetchData().then((result) => {
setData(result); // updates data...
});
}, [data]); // ...but data is in the dep arrayThe fix is to remove the state variable you are updating from the dependency array, and only include the values that should trigger a new fetch.
// Good — runs once on mount, no loop
const [data, setData] = useState([]);
useEffect(() => {
fetchData().then((result) => {
setData(result);
});
}, []); // empty array = run only on mountIf you need to refetch when something changes, use a trigger variable — not the state being updated:
// Good — refetch when userId changes, not when data changes
useEffect(() => {
fetchData(userId).then(setData);
}, [userId]); // userId is the trigger, not dataPattern 2: Objects or Arrays in the Dependency Array
React uses reference equality (===) to compare dependency values. Objects and arrays are recreated on every render, so even if the content is identical, React sees them as new values.
// Bad — config is a new object reference on every render
const config = { userId: "123", limit: 20 };
useEffect(() => {
fetchUserData(config);
}, [config]); // always "new", always loopsThe cleanest fix is to decompose the object into primitive values:
// Good — primitive values use value comparison, not reference
const userId = "123";
const limit = 20;
useEffect(() => {
fetchUserData({ userId, limit });
}, [userId, limit]);Alternatively, if the object must exist as a variable, memoize it with useMemo:
import { useMemo } from "react";
const config = useMemo(() => ({ userId, limit }), [userId, limit]);
useEffect(() => {
fetchUserData(config);
}, [config]); // stable reference nowPattern 3: Functions Defined Inside the Component Are in the Dependency Array
Functions are also compared by reference. A function defined inside the component body is a new object on every render.
// Bad — loadData is recreated on every render
const loadData = () => {
fetchData().then(setData);
};
useEffect(() => {
loadData();
}, [loadData]); // always new reference — loopsTwo solutions: move the function outside the component, or memoize it with useCallback.
// Good option 1 — move outside the component entirely
const loadData = (setData) => {
fetchData().then(setData);
};
const MyComponent = () => {
const [data, setData] = useState([]);
useEffect(() => {
loadData(setData);
}, []); // setData from useState is stable
};// Good option 2 — useCallback inside the component
import { useCallback } from "react";
const MyComponent = () => {
const [data, setData] = useState([]);
const loadData = useCallback(() => {
fetchData().then(setData);
}, []); // stable reference
useEffect(() => {
loadData();
}, [loadData]);
};Pattern 4: Blindly Adding Everything the Linter Warns About
The ESLint exhaustive-deps rule will warn when values used inside a useEffect are missing from the dependency array. The tempting fix is to add everything the linter suggests — which can create a loop if one of those values is set inside the same effect.
Before adding a value to the dependency array, ask: "Do I actually want the effect to re-run when this value changes?"
- Yes → add it to the array, but verify it does not cause a state update cycle
- No → either keep it out (if safe), or use
useRefto access the current value without triggering re-runs
// useRef gives you the latest value without adding it to the dep array
import { useRef } from "react";
const MyComponent = ({ onSuccess }) => {
const onSuccessRef = useRef(onSuccess);
useEffect(() => {
onSuccessRef.current = onSuccess;
}); // no dep array = sync on every render (intentional)
useEffect(() => {
doAsyncWork().then(() => {
onSuccessRef.current(); // call via ref
});
}, []); // no loop — onSuccess is not in the dep array
};Pattern 5: Unstable Callback Props From a Parent
Rork often generates child components that call a parent-supplied callback inside useEffect. If the parent creates the function inline, it is unstable.
// Bad — parent creates a new function on every render
const Parent = () => {
return (
<Child onDataLoaded={(data) => console.log(data)} />
// ↑ new function reference every render
);
};
const Child = ({ onDataLoaded }) => {
useEffect(() => {
fetchData().then(onDataLoaded);
}, [onDataLoaded]); // loops because onDataLoaded keeps changing
};Fix the parent with useCallback:
import { useCallback } from "react";
const Parent = () => {
const handleDataLoaded = useCallback((data) => {
console.log(data);
}, []); // stable reference
return <Child onDataLoaded={handleDataLoaded} />;
};How to Debug a Loop Systematically
When you are not sure which effect is looping, use this three-step approach.
Step 1: Add a timestamp log. If the same timestamp floods the console, that is your effect.
useEffect(() => {
console.log("effect ran at:", Date.now());
}, [userId, data]);Step 2: Log what changed. Build a simple prev-value tracker:
import { useRef, useEffect } from "react";
const prevRef = useRef({});
useEffect(() => {
const keys = ["userId", "data"];
keys.forEach((key) => {
if (prevRef.current[key] \!== eval(key)) {
console.log(`${key} changed:`, prevRef.current[key], "->", eval(key));
}
});
prevRef.current = { userId, data };
}, [userId, data]);Step 3: Comment out setState calls one by one. Disable each state update inside the effect until the loop stops. That is your culprit.
Prompting Rork to Fix the Loop
When asking Rork AI to resolve this, be specific:
My useEffect is running in an infinite loop. The effect calls
setData(result) but data is also in the dependency array, which
causes it to re-run on every state update.
Please fix this so the fetch only runs on mount (or when userId
changes). Do not suppress the ESLint warning with a disable
comment — fix the dependency array correctly.
Here is the component:
[paste your code]
Explicitly telling Rork not to use eslint-disable is important. AI assistants will sometimes silence the warning rather than address the root cause.
The Rule of Thumb
When you suspect an infinite loop, check the dependency array for three things:
- Is the state variable being updated inside the effect also listed as a dependency?
- Are any objects, arrays, or functions in the dependency array that are recreated on every render?
- Is a callback prop included that the parent creates inline?
Resolving any of these with useCallback, useMemo, useRef, or by restructuring the logic will stop the loop. For deeper patterns around state architecture in Rork apps, see Advanced State Management Patterns for Rork Apps. For broader performance issues, Rork App Performance Optimization Complete Guide covers profiling and memory management as well.