useEffect and asynchronous code
1. A React component must be a pure function
A component in React is a pure function that:
- accepts input data (
props); - returns a description of the UI (
JSX); - must not have side effects (for example, network requests, timers, logs, and so on).
A pure function is a function that, given the same inputs, always returns the same result, and does not change external state.
Asynchronous requests (fetch, axios, WebSocket, and so on) are side effects,
they interact with the outside world (the network, an API, disk, the browser).
That is why React forbids running side effects during render (inside the component function's body), so as not to break the principle of predictability.
2. useEffect() runs after the render
When React calls the component, it:
- calls the function -> gets JSX (a pure result);
- paints that JSX into the DOM;
- and only then - runs the side effects registered through
useEffect().
This is the ideal place for asynchronous operations because:
- the UI is already mounted (you can show "Loading...");
- you can safely update state without affecting the current render.
Example:
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data));
}, []);Here React first shows the component with Loading...,
and after the request finishes it updates the state and re-renders the updated UI.
3. You cannot write asynchronous code directly in the component
If you try:
function User() {
const res = await fetch('/api/user'); // Error
const data = await res.json();
return <div>{data.name}</div>;
}React cannot await await in client components (except Server Components).
It expects the component to synchronously return JSX, not a promise.
So this will cause an error.
useEffect() is exactly what lets you defer the execution of such asynchronous operations
until React finishes the render phase.
4. Controlling dependencies
useEffect accepts a second argument - a dependency array:
useEffect(() => {
fetch(`/api/users/${id}`)
.then(r => r.json())
.then(setUser);
}, [id]);This way you can:
- call the request only once (
[]); - or only when specific data changes (
[id]).
This gives you full control over exactly when the request runs.
Without useEffect you would not have this control - requests would fire on every render.
5. The ability to clean up (cleanup)
If the component unmounts while data is loading,
useEffect lets you clean up the effect so you do not update state on an unmounted component:
useEffect(() => {
let active = true;
fetch('/api/data')
.then(r => r.json())
.then(data => {
if (active) setData(data);
});
return () => { active = false }; // cleanup
}, []);Without useEffect this would be impossible to implement correctly.
6. React 18 and asynchronous renders
React 18 introduced asynchronous rendering (Concurrent Rendering). With it, a render can be:
- paused,
- interrupted,
- retried.
If asynchronous requests ran during render, React would not be able to safely interrupt them - this would cause data races, leaks, and duplicate requests.
useEffect solves this - it runs after a successful commit phase,
that is, only when the component has actually been painted.
SUMMARY
| What happens | Why it matters |
|---|---|
| The component must be a pure function | Asynchronous operations are side effects |
useEffect() runs after the render | It is safe to make requests |
The second [] argument controls the frequency | You can avoid unnecessary requests |
| The effect can be cleaned up | No leaks or data races |
| Compatible with Concurrent Rendering | React can interrupt and retry renders without issues |
In short:
Asynchronous requests run inside
useEffect()because it is the only place where React allows side effects, after the render has finished, with control over dependencies and the ability to clean up.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.