Anything an effect starts — a subscription, a timer, a fetch, an event listener — outlives the component unless the effect returns something that stops it.
useEffect(() => {
const controller = new AbortController();
fetch(`/api/orders/${id}`, { signal: controller.signal })
.then(r => r.json())
.then(setOrder)
.catch(e => { if (e.name !== 'AbortError') report(e); });
return () => controller.abort();
}, [id]);
The cleanup runs before every re-run of the effect as well as on unmount, which is the part people miss — an effect with a changing dependency cleans up and re-runs on every change. Without the abort, a component that unmounts mid-request sets state on something that no longer exists and React warns about it. Distinguishing the abort error from a real failure is required, or every cancellation is reported as a bug.