How would you use useEffect to trigger the API request and manage cleanup
Fetch inside useEffect with the right dependency array; in the cleanup function, abort the request (AbortController) or set an `ignore` flag so a stale/late response can't update an unmounted or superseded component. Handle loading/error states; consider a query library for real apps.
Fetching in useEffect is straightforward; cleanup is the part interviewers actually grade — it's what prevents stale-response and unmount bugs.
The full pattern
function User({ userId }) {
const [state, setState] = useState({ status: "loading", data: null, error: null });
useEffect(() => {
const controller = new AbortController();
setState({ status: "loading", data: null, error: null });
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then((data) => setState({ status: "success", data, error: null }))
.catch((err) => {
if (err.name === "AbortError") return; // expected — ignore
setState({ status: "error", data: null, error: err });
});
return () => controller.abort(); // CLEANUP
}, [userId]); // re-run when userId changes
// ...
}The three things being tested
1. The dependency array. List every value the request depends on (userId). Omit it → fetches every render (infinite loop risk). Empty when it shouldn't be → stale data when the input changes.
2. Cleanup — the core of the question. The cleanup runs before the next effect run and on unmount. Two ways to make stale responses harmless:
AbortController(preferred) — actually cancels the request.ignoreflag — if you can't abort:
useEffect(() => {
let ignore = false;
fetchData().then((d) => { if (!ignore) setState(...); });
return () => { ignore = true; };
}, [userId]);Without cleanup you get two real bugs: setState on an unmounted component, and the race condition where a slow response for an old userId resolves after a fast response for the new one and overwrites it.
3. UI states — loading / error / success, not just the happy path.
What to also say
For real apps, don't hand-roll this — React Query/SWR handle the effect, cleanup, caching, deduping, and race conditions for you. The manual version is the interview exercise; the production answer is a query library.
The framing
"I fetch inside useEffect with a dependency array listing everything the request depends on. The graded part is cleanup: I return a function that calls controller.abort() — or flips an ignore flag — so when the component unmounts or the dependency changes before the response lands, a stale response can't update the component. That kills both the setState-after-unmount warning and the out-of-order-response race. I also model loading/error/success states — and in a real app I'd reach for React Query so I'm not hand-rolling any of this."
Follow-up questions
- •What race condition does the cleanup prevent?
- •AbortController vs an `ignore` flag — when would you use each?
- •What goes wrong if the dependency array is missing or wrong?
- •Why would you use React Query instead of this pattern?
Common mistakes
- •No cleanup — setState-after-unmount and out-of-order responses.
- •Missing or wrong dependency array — infinite fetch loop or stale data.
- •Not handling the error and loading states.
- •Making the effect callback itself async (it must return a cleanup fn, not a promise).
Performance considerations
- •Cancelling superseded requests avoids wasted parsing and discarded renders. A wrong dependency array can cause an infinite fetch loop — the most damaging perf bug here.
Edge cases
- •Dependency changes rapidly (search input) — each run aborts the previous.
- •Component unmounts before the response arrives.
- •Request succeeds but the component already moved on.
- •Non-OK HTTP status — fetch doesn't reject on 404/500, you must check res.ok.
Real-world examples
- •Loading a detail view when the selected id changes.
- •Search results that must not show a stale query's response.