Skip to main content

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:

  1. calls the function -> gets JSX (a pure result);
  2. paints that JSX into the DOM;
  3. 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:

javascript
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:

javascript
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:

javascript
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:

javascript
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 happensWhy it matters
The component must be a pure functionAsynchronous operations are side effects
useEffect() runs after the renderIt is safe to make requests
The second [] argument controls the frequencyYou can avoid unnecessary requests
The effect can be cleaned upNo leaks or data races
Compatible with Concurrent RenderingReact 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.