A React component loads its data by calling `fetch` inside `useEffect` and storing the result with `useState`. Why does that component always render at least once with no data, and what does that force you to write by hand?
answer
- render first, request second
- effects run after the commit
- initial state is a placeholder
- four branches nobody writes for you
- a 404 still fulfils the promise
basics
~10 sEffects run after React has rendered and committed the component, so the first render happens before the request is even sent. Every fetch-in-effect component therefore needs hand-written loading, error and empty states.
solid answer
~50 sReact calls the component, commits the result to the DOM, and only then runs the effect — so the first render happens before the request is sent, with whatever the initial state was. That means the component has to render a state where there is no data yet, and in practice at least four: nothing yet, in flight, failed, and loaded. `useEffect` gives you none of that, so you hand-roll it: an `isLoading` flag, an `error` slot, a `data` slot, and the ordering rules between them, in every component that fetches. The boilerplate is repetitive and easy to get subtly wrong — a spinner that never clears after a rejection, a stale error left over from a previous attempt, a `TypeError` from reading a field off `null`. That repetition is a big part of why fetching moved out of effects.
code
jsx · 22 linesimport { useEffect, useState } from 'react';
export function UserCard() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch('/api/user')
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(setData)
.catch(setError)
.finally(() => setIsLoading(false));
}, []);
if (isLoading) return <p>Loading</p>;
if (error) return <p>Could not load user.</p>;
return <h2>{data.name}</h2>;
}go deeper
Be ready to state the order plainly: render, commit, then effect. Say that the first render sees the initial state, so the JSX must handle no-data, and that you write the loading and error flags yourself.
Explain why the empty render is unavoidable rather than a coding mistake, and enumerate the states a real component tracks — idle, loading, error, empty success, and stale-while-refetching. Mention that fetch fulfils on a 500.
Show you have seen the failure modes in production: spinners that never clear, stale error messages after a successful retry, and inconsistent loading UX because every component reimplemented the same branches. Connect that cost to the decision to centralise fetching.
Frame it as a codebase-wide policy question: an ad-hoc state machine per component is unreviewable and untestable at scale. Be ready to argue for one loading and error contract across the app, and to say what you would give up to get it.
## The order React actually runs things When React renders a function component it calls the component function, compares the returned element tree against the previous one, and commits the differences to the DOM. Only after that commit does it run the callbacks passed to `useEffect` — and for these passive effects it runs them after the browser has had a chance to paint. Nothing in that sequence waits on you. React does not know your effect is about to start a network request, and it has no mechanism for pausing a render until an effect resolves. So the timeline for `useEffect(() => { fetch(...) }, [])` is: render → commit → paint → effect runs → request leaves the browser → response arrives some time later → you call `setData` → React schedules a second render → the second render finally shows data. ## Why the first render is necessarily empty Because the request has not been sent when the component function first runs, your state hook must be initialised with a placeholder — `null`, `undefined`, or `[]`. The very first return of your JSX is evaluated against that placeholder. This is not a mistake you can code around inside the effect: it is a direct consequence of *when* effects run. Writing `fetch` in the component body instead does not help — the body runs during render, must be synchronous and side-effect free, and would fire a new request on every render. ## The state machine you inherit A fetch has more than two outcomes, so a realistic component tracks: - **idle / not yet requested** — the first render, before the effect fires - **in flight** — show a spinner or skeleton - **error** — the request rejected or the response was not `ok` - **success with data** — the happy path - **success with empty data** — a `200` that returned zero rows, which is not the same as "still loading" - **refetching with stale data on screen** — the inputs changed but the old result is still rendered ```jsx if (isLoading) return <Spinner />; if (error) return <ErrorNotice />; if (data.length === 0) return <EmptyState />; return <List items={data} />; ``` Nothing in React authors those branches for you, and the order in which you check them is itself a decision — check `error` before `isLoading` and a retry will flash the old error. ## Where the bugs come from The common defects are all in the hand-written glue, not in `fetch`: - Reading `data.name` without a guard, because the author forgot the first render sees `null`. - Not clearing `error` when the inputs change, so a recovered request still shows a failure message. - Not setting `isLoading` back to `false` on the failure path, leaving a spinner forever. - Conflating `null` (never loaded) with `[]` (loaded, empty), so the user sees "No results" for a split second before results appear. - Every component in the codebase implementing the same five branches slightly differently, so the loading and error experience is inconsistent screen to screen. Note also that a failed response is not a rejected promise: `fetch` fulfils normally for a `404` or `500`, so if you do not inspect `response.ok` yourself the error branch never runs and you try to render an error payload as data. ## What moving the fetch out changes The alternatives all attack the same root cause — that the component starts rendering before anything has been requested. A caching query library returns the state machine to you as values instead of making you assemble it from three `useState` calls, and it applies one consistent policy across the app. A router loader resolves the data *before* the route component renders, so the component receives data rather than a placeholder. A server component `await`s during render, so the markup that reaches the browser already contains content, and a `Suspense` boundary hoists the loading state out of the component into the tree, while an error boundary does the same for failures — which is why in those models a component often has no `isLoading` branch at all. The interview point is the causal chain, not the product recommendation: the empty first render is a property of effect timing, so the only real fix is to stop starting the request from an effect.
- Why can you not just call fetch in the component body to have the data ready on the first render?The component body runs during render and must be synchronous and free of side effects — it can return elements, not wait. Calling `fetch` there would fire a fresh request on every render, including the re-render your own `setState` causes, and React may call the function more than once per commit or discard the result. Render must stay pure; starting requests is exactly what effects and, in newer models, server-side rendering are for.
- Your component shows a spinner forever when the API returns a 500. What is the likely bug?Two candidates. First, `fetch` only rejects on a network-level failure, so a `500` fulfils normally and never reaches your `.catch` unless you check `response.ok` and throw yourself. Second, if the error path does run but `setIsLoading(false)` lives only in the success branch rather than in `finally`, the flag never clears. Both leave the loading branch rendering indefinitely.
- What is the difference between data being null and data being an empty array, and why does it matter to the user?`null` means "we have not loaded yet"; `[]` means "we loaded and there is genuinely nothing". If you render the same empty state for both, the user sees "No results found" while the request is still in flight, then a flash as results appear. Distinguishing them is why an explicit loading flag or a discriminated status value beats inferring the state from the data itself.
It is like a waiter who seats you, hands you the menu, and only then walks to the kitchen to ask what is available — you are guaranteed to spend some time at the table with nothing in front of you.
saying these in an interview costs you the question
- Says useEffect runs before the first render
- Claims React waits for the effect before painting
- Thinks calling fetch in the component body fixes it
- Assumes fetch rejects on a 404 or 500 response
- Renders data.name with no null check on first render