Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our course on migrating your React SPA to a modern SSR architecture.
In our last lesson, we successfully mapped out the new route structure for your application, translating the familiar patterns of React Router into Tanstack Router's powerful file-based conventions. We now have a solid foundation of routes, but they are still just empty shells.
Today, we will breathe life into these routes by tackling one of the most critical aspects of SSR: data fetching. This lesson is designed to meet the learning outcome: Convert client-side data fetching patterns (e.g., useQuery in components) to server-side fetching in route loaders.
We'll move the responsibility of fetching initial data from the client-side components to the routes themselves. This shift is fundamental to achieving the performance and SEO benefits of server-side rendering. You'll learn how to fetch data on the server before a page is rendered and how to make that data available to your React components seamlessly.
1. The "Why": Server vs. Client Data Fetching
In a traditional SPA, a component is responsible for its own data. It mounts, shows a loading state, makes an API call, and then renders the data once it arrives.
// A typical client-side data fetching pattern in a component
function UserProfile() {
const { data, isLoading } = useQuery({
queryKey: ['user'],
queryFn: fetchUser
});
if (isLoading) {
return <Spinner />;
}
return <h1>Welcome, {data.name}</h1>;
}
This pattern is robust but has a key drawback for initial page loads: the user receives a nearly blank HTML page and must wait for multiple network round-trips (JS bundle -> data fetch) before seeing meaningful content.
Server-Side Rendering with route loaders flips this model. The server fetches the necessary data first and then renders the complete HTML page with the data already included. The browser receives content it can display immediately, dramatically improving the perceived performance and making the content available to search engine crawlers.
This article from Frontend Masters provides an excellent high-level overview of why server rendering is beneficial and introduces the core problem that Tanstack Start's architecture solves.
Read the section 'Why Server Rendering?'. It clearly illustrates the network waterfall of a client-rendered app versus an SSR app, setting the stage for why we're moving data fetching to the server.
2. The "How": Route Loaders
Tanstack Router's solution to server-side data fetching is the loader function. A loader is an asynchronous function you define on a route that runs before the route's component is rendered.
Data Loading | TanStack Router React Docs
Let's start with the official definition. The Tanstack Router documentation introduces the concept of loaders and their role in the routing lifecycle.
Read the introduction, 'The route loading lifecycle', and 'Route loaders' sections. This will give you a solid theoretical understanding of what loaders are and where they fit into the process.
The key steps to convert a client-side fetch are:
- Define a
loaderfunction in your route file (e.g.,src/routes/posts/$postId.tsx). - Move the data-fetching logic into this
loader. - Access route parameters (like
postId) from theparamsobject passed to the loader. - Return the fetched data from the loader.
- In your component, remove the
useQueryhook and instead access the pre-fetched data using theuseLoaderDatahook.
This video provides a perfect practical demonstration of this entire process.
Tanstack Router Tutorial - Routing, Lazy Loading, Data Fetching, Params...
Watching this conversion in action is the best way to understand it. PedroTech walks through creating a dynamic route, adding a loader to fetch data based on the route parameter, and then consuming that data in the component.
Watch the loader function. Pay close attention to how the fetch call is moved into the loader function and how useLoaderData is used to retrieve the result in the component. This is the core pattern you will be using.
Here is a before-and-after comparison of the code for a dynamic post route:
Before: Client-Side Fetching with useQuery
// src/routes/posts/$postId.tsx
import { useQuery } from '@tanstack/react-query';
// ... route setup ...
function PostComponent() {
const { postId } = Route.useParams();
const { data, isLoading } = useQuery({
queryKey: ['posts', postId],
queryFn: () => fetch(`https://api.example.com/posts/${postId}`).then(res => res.json())
});
if (isLoading) return <div>Loading...</div>;
return <h1>{data.title}</h1>;
}
After: Server-Side Fetching with loader
// src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/posts/$postId')({
// 1. Loader fetches data before component renders
loader: async ({ params }) => {
const { postId } = params;
const post = await fetch(`https://api.example.com/posts/${postId}`).then(res => res.json());
return { post }; // 2. Data is returned
},
component: PostComponent,
});
function PostComponent() {
// 3. Component receives data, no loading state needed for initial render
const { post } = Route.useLoaderData();
return <h1>{post.title}</h1>;
}
3. Server-Only Logic with createServerFn
In the example above, the fetch call is simple. But what if you need to connect to a database directly or use secret API keys? This logic must never be sent to the client.
This is where Tanstack Start provides a crucial utility: createServerFn. It allows you to define a function that is guaranteed to only ever execute on the server. The build process replaces the function body with a network call, effectively creating a type-safe RPC (Remote Procedure Call) mechanism.
Your loader can then safely call this server function. This pattern is central to building secure SSR applications with Tanstack Start.
The 'Introducing TanStack Start' article explains this concept well, showing how to create a server function and call it from a loader.
In the section 'Loading Data', read server function examples. Focus on how createServerFn is used to define getTasksList and how this function is then simply await-ed inside the route loader. This is the standard pattern for data fetching in Tanstack Start.
Here's how our previous example would look using this pattern:
// src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/start';
// This function is guaranteed to run ONLY on the server
const fetchPostById = createServerFn('GET', async (postId: string) => {
// You could use secret keys or direct DB access here
const res = await fetch(`https://api.example.com/posts/${postId}`);
if (!res.ok) throw new Error('Failed to fetch post');
return res.json();
});
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
// The loader calls the server function
const post = await fetchPostById(params.postId);
return { post };
},
component: PostComponent,
});
function PostComponent() {
const { post } = Route.useLoaderData();
return <h1>{post.title}</h1>;
}
4. A Spectrum of Choices: When to Fetch Where
Moving data fetching to loaders is powerful, but it's not an all-or-nothing decision. For a rich user experience, you might not want to block the entire page render for every piece of data. Tanstack Start gives you a spectrum of options.
The 3 ways to fetch data in TanStack start
This video by Web Dev Cody provides a concise and clear comparison of the three primary data fetching strategies in a Tanstack Start application. Understanding these options is key to making the right architectural decisions.
Watch the first few minutes, which cover: (1) Server-side fetching with a loader (blocking), (2) Client-side fetching with useSuspenseQuery, and (3) Traditional client-side fetching with useQuery. Note the different user experiences each approach creates.
Here's a summary of the strategies:
-
Blocking SSR (Loader +
await):- How:
awaityour data fetch inside theloader. - When: For critical data essential for the initial view (e.g., the main content of an article).
- Pro: Great for SEO; no layout shifts.
- Con: Increases Time to First Byte (TTFB) if the data fetch is slow. The user sees a blank screen until the data is ready.
- How:
-
Streaming SSR (Loader without
await+ Suspense):- How: Call your server function in the loader but don't
awaitit; return the promise. In the component, wrap the UI that needs the data in a<Suspense>boundary. - When: For slower, non-critical data on the page (e.g., a "related articles" sidebar).
- Pro: Fast TTFB. The main page shell renders immediately with a loading state for the deferred content.
- Con: Requires managing loading/fallback states.
- How: Call your server function in the loader but don't
{"type":"reading","par_intro":"The Frontend Masters article provides a great explanation and code example for implementing streaming.","par_directions":"Scroll down to the second 'Streaming' heading (under 'TanStack Start'). Read <span data-type="resource_reading_textrange" data-resource-subitem-id="d76c35d7" data-range-start="Let’s make the individual task loading purposefully slow" data-range-end="provide data for our component.">the 'Streaming' section. Notice the key difference in the loader: getTask is called without await. Then see how the use hook consumes the promise in the component, triggering the Suspense boundary.","learning_duration":"4 minutes","url":"https://frontendmasters.com/blog/introducing-tanstack-start/","title":"Introducing TanStack Start","isV2":true,"blockId":"8c03b8da-eed1-4053-8630-0158a76a1e52","lessonId":"3158c725-67a0-42ab-a79d-30f1caa1fbf7"}
3. **Pure Client-Side Fetching (`useSuspenseQuery` or `useQuery`)**:
* **How**: Don't use a loader. Fetch data directly in the component, just like in an SPA.
* **When**: For data that is highly dynamic, user-specific, or not needed at all for the initial render (e.g., content in a modal opened by the user).
* **Pro**: Decouples the data from the route.
* **Con**: Not SEO-friendly; can cause layout shifts if not handled with Suspense.
Your migration strategy will involve analyzing each data requirement and choosing the appropriate pattern from this spectrum.
### Conclusion
Today you've learned the fundamental pattern for migrating data fetching from your components to the server. This is the heart of building a performant SSR application.
**Key Takeaways:**
* **Route `loader` functions** are the primary mechanism for fetching data on the server before a component renders.
* **`createServerFn`** is the essential tool for creating secure, server-only logic that can be called from your universal `loader` functions.
* **`useLoaderData`** is the hook used in components to access the data pre-fetched by the loader.
* You have a **spectrum of fetching strategies**: blocking SSR for critical data, streaming SSR for slower secondary data, and client-side fetching for dynamic, interactive data. Choosing the right one is a key architectural decision.
**Next Up:**
We've covered how to *read* data on the server. The next logical step is to handle *writing* data. In our next lesson, we will focus on how to **transform client-side data mutations into server functions while maintaining React Query's client-side cache integration.** This will complete the data lifecycle in our new SSR architecture.