Topic 14.1
How React Apps Get Data
In one line
React apps load data in four main ways: in effects, through a data library, in route loaders, or in server components; the big difference between them is when a request starts, and starting late causes slow request waterfalls.
Think of it like this
Cooking dinner. A slow cook boils the rice, waits for it to finish, and only then starts chopping the vegetables. A smart cook starts the rice, chops while it boils, and everything is ready at the same time. Loading data is the same: if each request waits for the one before it, the page is slow. Start everything you already know you'll need at once.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Data fetching
- Asking a server for data, usually with
fetch, and showing the answer on the screen. - Promise
- A JavaScript object that will hold a value later, or an error if the work fails.
- Cache
- A store of earlier answers, so the same data doesn't have to be loaded again.
- Data library
- A package such as TanStack Query or SWR that fetches, caches and refreshes server data for your components.
- Route loader
- A function a router runs before showing a page, to load that page's data.
- Server component
- A component that runs on the server, can
awaitdata directly and sends only its result to the browser (Phase 20). - Request waterfall
- Requests that run one after another although they could run at the same time, making the page slower.
- Fetch-on-render
- A component starts loading its data only when it renders.
- Render-as-you-fetch
- Start loading first (on a click or in a loader), and render while the data is on its way.
Step by step
01The effect version and its timeline
A Profile loads the user in an effect. Once it has the user, it renders Posts, which loads posts in its own effect. Read the order: render, effect, wait, render the child, effect, wait.
02See the waterfall in the console
The first live example below logs when each request starts and finishes, rounded to 500 ms (the time of one request). Posts can't start before 500 ms, because the Posts component doesn't exist until the user has loaded. That one-after-another shape is the waterfall.
03Fix it by starting both requests in one place
Posts don't depend on the user here (both only need the user id from the URL), so the parent can start both requests together with Promise.all and pass the results down. Both finish at 500 ms. The child no longer fetches: it receives data as props.
useEffect(() => {
Promise.all([getUser(id), getPosts(id)]).then(([user, posts]) =>
setData({ user, posts }),
);
}, [id]);04Move the start even earlier: a route loader
A data router runs the loader when the link is clicked, before any component of the new page renders. The old page stays on screen and useNavigation().state is "loading" so you can show a progress bar. The third live example shows this with a memory router.
{
path: "profile/:id",
loader: async ({ params }) => {
const [user, posts] = await Promise.all([getUser(params.id), getPosts(params.id)]);
return { user, posts };
},
element: <Profile />, // reads it with useLoaderData()
}05Know the server option
With React Server Components (Phase 20) the component itself is async and awaits the data on the server. No effect, no loading state for the first view, no API call from the browser. You need a framework such as Next.js for it.
// Runs on the server only (a Server Component, Phase 20).
export default async function ProfilePage({ params }) {
const { id } = await params;
const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);
return <Profile user={user} posts={posts} />;
}06Pick by where your app runs
A client-only app (Vite) usually uses a data library, plus loaders if it uses a data router. A Next.js or React Router framework app loads most data on the server or in loaders, and uses a data library for highly interactive client data. Plain effects are for small cases and for learning how it works underneath.
Try it yourself
- 1
Make the waterfall deeper
In the first example, add a
Commentscomponent insidePoststhat fetchescommentsin its own effect. Predict the start time of comments before you run it. (Answer: 1000 ms, finished at 1500 ms.) - 2
Make one request slower
In the second example, give
fakeApiamsparameter and make posts take 1000 ms. When does the page appear now? WithPromise.all, the slowest request decides. Think about whether showing the user first and the posts later would be nicer: that is what nested Suspense boundaries do (Topic 14.3). - 3
Remove the loading bar
In the loader example, delete the
navigation.state === "loading"line. The click now seems to do nothing for 300 ms. Put it back: on slow networks, a visible sign that the click worked matters.
Code & diagrams
Posts start at 500 ms, after the user arrived, so the page needs 1000 ms for two 500 ms requests. (Times are rounded to 500 ms. A real project with StrictMode would also log each start twice in development: Topic 7.4.)
Preview
Console
Nothing logged yet.Expected console output
start user at 0 ms
got user at 500 ms
start posts at 500 ms
got posts at 1000 msBoth requests start at 0 ms and the page is ready at 500 ms: twice as fast, with the same server.
Preview
Console
Nothing logged yet.Expected console output
start user at 0 ms
start posts at 0 ms
got user at 500 ms
got posts at 500 msBoth requests start at the click. While they load, the old page ("Pick a page.") stays visible with a loading bar, and `Profile` renders once, with its data.
Preview
Console
Nothing logged yet.Expected console output
start user
start posts
while loading: Home | ProfileLoading...Pick a page.
done: Home | Profileuser dataposts dataBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Fetch in a child that waits for its parent
In the second example, move the posts request back into an effect inside Posts and only start the user request in Profile.
Break #2
Forget that effects don't run on the server
Render the first example with renderToString from react-dom/server (or as a server-rendered page in a framework) and look at the HTML.
Myth vs fact
Myth
useEffect is the standard way to fetch data in React.
Fact
It works and is good for learning, but React's docs recommend a framework's data loading or a data library for real apps, because effects start late, cause waterfalls, don't cache and don't run on the server.
Myth
Waterfalls only matter on slow servers.
Fact
Every round trip includes network latency (often 50 to 300 ms on mobile). Three chained requests pay it three times, however fast the server is.
Myth
Render-as-you-fetch needs a special library.
Fact
It only means starting the request before rendering: in an event handler, a loader or at module level, then reading the promise with use() or a library. React Router loaders and server components do it for you.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Not every waterfall is a bug: when request B needs a value from response A (the user's
orgIdto load the org), they must be sequential. Then move the chain to the server (one endpoint, or a loader, or a server component) so it costs server-to-database latency instead of browser-to-server latency. - ▸
Code splitting can hide a waterfall:
lazy()loads a component's code, and only then does the component render and start its fetch. Start the data request in parallel with the code (in the loader or the click handler), or use route-levellazyin React Router, which loads code and data together (Topic 16.8). - ▸
Data libraries are fetch-on-render by default, but they support render-as-you-fetch:
queryClient.prefetchQuery()in a loader or on hover, thenuseQueryreads the warm cache. TanStack Router and React Router examples use exactly this combination. - ▸
The DevTools Network panel's waterfall column shows the shape of your requests at a glance. Throttle to "Slow 4G" while developing: waterfalls that cost 20 ms locally cost seconds on a phone.
Remember this
- 1
Data fetching means asking a server for data (usually over HTTP with
fetch(url), see /system-design for HTTP and APIs) and showing the answer. The request is asynchronous:fetchreturns a promise, an object that will hold the answer later (see /javascript promises and async/await). React itself doesn't fetch anything. It renders whatever is in state or props right now. So the real questions are: who starts the request, when, and where the answer is kept while the user moves around the app. - 2
Way 1: fetch in an effect (Topic 7.7). A component renders, then
useEffectstarts the request, thensetStatestores the answer and the component renders again. It needs no library and is fine for small apps and learning. Its weaknesses: the request starts only after the first render (so the user always sees a loading state first), nothing is cached (leave and come back and it loads again), you hand-write loading, error and race handling (Topic 14.7), and effects never run on the server. - 3
Way 2: a data library such as TanStack Query (Topic 14.6), SWR, Apollo (for GraphQL) or RTK Query (Topic 15.4). You still ask for data from inside a component (
useQuery({ queryKey, queryFn })), but the library keeps a shared cache (a store of earlier answers), so two components asking for the same data share one request, going back shows cached data instantly, and the library refreshes stale data in the background. It handles retries, race conditions and loading flags for you. This is the most common choice in client-rendered apps. - 4
Way 3: route loaders. A router (Phase 13) knows which page the user is going to before it renders. Data routers in React Router let each route declare a
loaderfunction (Topic 13.5). When the user clicks a link, the router runs the loaders for every route on the new page in parallel, keeps the old page on screen meanwhile, and renders the new page withuseLoaderData()once the data is there. Requests start at the click, not after rendering, and nested routes never wait for each other. - 5
Way 4: server components (Phase 20). In frameworks like Next.js, a component can run on the server as an
asyncfunction andawaitits data directly:const posts = await db.posts.findMany(). The data never travels as a separate API call from the browser, secrets stay on the server, and the HTML can stream to the browser as soon as each part is ready (using Suspense, Topic 14.3). This is React's recommended direction for new full-stack apps, and it needs a framework. - 6
Request waterfalls. A waterfall is a chain of requests where each one starts only after the previous one finished, even though they didn't depend on each other. With effects it happens naturally:
Profileloads the user, shows a spinner, then rendersPosts, whose effect only now starts loading posts. Two 300 ms requests take 600 ms instead of 300 ms. Deep trees make it worse: five levels of components fetching in effects can mean five round trips. The cure is to start requests earlier and together: lift them up to one place (Promise.all), use route loaders, or prefetch. - 7
Fetch-on-render vs render-as-you-fetch. Fetch-on-render means a component starts its own request when it renders (effects, and also most
useQuerycalls): simple, but it creates waterfalls when parents and children both fetch. Render-as-you-fetch means you start the request first (on the click, in a loader, at module load or in an event handler), and then render right away with a promise that components read when it's ready (withuse()and Suspense, Topic 14.4). React's newer features (loaders,use, server components, Suspense) are all built for render-as-you-fetch. A third name you'll see, fetch-then-render, means "load everything, then render the whole page": no waterfall, but nothing shows until the slowest request is done.
Explain it without notes
Name the four main ways a React app gets data and one strength and one weakness of each.
What is a request waterfall, why do effects tend to create them, and how do you fix one?
Explain fetch-on-render, fetch-then-render and render-as-you-fetch.
Why does React's documentation steer people away from fetching in effects for production apps?
Practice
A Dashboard renders <Stats /> and <Alerts />; each fetches in its own effect. Is this a waterfall? Explain, then write a version where Alerts only renders after stats load and say what changes.
Rewrite this to start both requests at once: const user = await getUser(id); const orders = await getOrders(id);.
Write a React Router route object with a loader that loads a product and its reviews in parallel, and the component that reads them.
Trade-offs
- ↔
Effects need no library but give you no cache, no deduplication and no server rendering. A data library adds a dependency to your bundle and a cache model to learn, and removes a lot of hand-written code.
- ↔
Loading everything in one loader avoids waterfalls but makes the page wait for the slowest request. Streaming the slow parts separately (Suspense, or returning a promise from a loader for a non-critical part) shows the fast parts sooner at the cost of more loading states.
Done when you can
I can name the four ways React apps get data and when each fits.
I can spot a request waterfall in code and in the Network panel.
I can fix a waterfall by starting independent requests together.
I can explain fetch-on-render, fetch-then-render and render-as-you-fetch.
I can write a route loader that loads data in parallel.