Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to your eighth lesson in our course on mastering TanStack Start and Router.
In our last lesson, we delved into the critical distinction between server and client code execution. You learned that code in TanStack Start is isomorphic by default and how to use APIs like createServerFn to create secure, server-only logic. This foundation is essential for what we're covering today.
Lesson 8: Server-Side Data Fetching with Loaders
Today's Goal:
This lesson directly addresses the learning outcome: Implement server-side data fetching in route loaders with typed data returns.
We will build upon the concept of server functions by integrating them into TanStack Router's primary data-fetching mechanism: the loader. This is a cornerstone of Server-Side Rendering (SSR). You'll learn how to fetch data on the server before your page component renders and how to ensure that data is fully type-safe from your API call all the way to your JSX.
What we will cover:
- The
loaderFunction: The core of data fetching in TanStack Router. - The Core Pattern: Combining
loaderwithcreateServerFnfor secure, typed API calls. - Dynamic Data Fetching: Using route parameters to fetch data for specific pages.
- Accessing Loader Data: Using the
useLoaderDatahook to consume data in your components. - Context and Trade-offs: Comparing different data-fetching strategies in TanStack Start.
1. The loader Function: Your Data Gateway
In TanStack Router, every route can have a loader function. This is an async function that runs before the route's component is rendered.
As we discussed in the previous lesson, the loader is isomorphic:
- On the initial page load, it runs on the server. The data it fetches is used to render the HTML, which is then sent to the browser. This is SSR.
- On subsequent client-side navigations (e.g., clicking a
<Link>), the sameloaderfunction runs in the browser to fetch the data for the new page.
The primary benefit is that data is available the moment the component renders, which eliminates "flash of loading content" and is excellent for SEO.
2. The Core Pattern: Secure, Typed Data Fetching
Let's put theory into practice. The most robust pattern for fetching data from an external, protected API is to combine the loader with the createServerFn you learned about previously.
- The
createServerFnhandles the secure, server-only logic (e.g., using an API key fromprocess.env). - The
loadercalls this server function to get the data.
The official TanStack Start documentation includes a great tutorial that walks through this process.
Calling an external API using TanStack Start
This tutorial, 'Calling an external API using TanStack Start', demonstrates the fundamental workflow. We'll focus on defining data types and setting up the route loader.
Read 'Step 2: Defining Data Types' and 'Step 3: Creating the Route with API Fetch Function'. Pay close attention to how the Movie and TMDBResponse interfaces are defined and how the loader function is structured to call fetchPopularMovies.
Synthesizing the Pattern
The tutorial you just read presents a slightly simplified pattern. Let's refine it by explicitly using createServerFn to make our server-only logic even more distinct and reusable, reinforcing what we learned in Lesson 7.
Here is how you would structure the code to combine these concepts:
// src/lib/server/movies.ts
import { createServerFn } from '@tanstack/react-start'
import type { TMDBResponse } from '../types/movie'
// This function is guaranteed to only ever run on the server.
// The client will make an RPC call to it.
export const fetchPopularMovies = createServerFn(
'GET', // HTTP method for the RPC call
async (): Promise<TMDBResponse> => {
const token = process.env.TMDB_AUTH_TOKEN
if (!token) {
throw new Error('Missing TMDB_AUTH_TOKEN environment variable')
}
const API_URL = '...' // The API URL
const response = await fetch(API_URL, {
headers: {
accept: 'application/json',
Authorization: `Bearer ${token}`,
},
})
if (!response.ok) {
throw new Error(`Failed to fetch movies: ${response.statusText}`)
}
return (await response.json()) as TMDBResponse
},
)
Now, your route file becomes much cleaner and safer.
// src/routes/movies.tsx
import { createFileRoute } from '@tanstack/react-router'
import { fetchPopularMovies } from '../lib/server/movies' // Import the server function
export const Route = createFileRoute('/movies')({
// The loader calls the server function.
// On SSR, this is a direct function call.
// On client navigation, this is a network request.
loader: async () => {
const moviesData = await fetchPopularMovies()
return moviesData.results
},
component: MoviesPage,
})
function MoviesPage() {
// `useLoaderData` is fully typed! It knows the loader returns `Movie[]`.
const movies = Route.useLoaderData()
return (
<div>
<h1>Popular Movies</h1>
<ul>
{movies.map((movie) => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
</div>
)
}
Notice how Route.useLoaderData() automatically infers its return type from the loader. This end-to-end type safety is a major feature of TanStack Router.
3. Fetching Data for Dynamic Routes
Often, you need to fetch data based on a URL parameter, like an ID. TanStack Router makes this straightforward by passing route parameters to the loader.
The video below clearly demonstrates how to set up a dynamic route and access its parameters within the loader.
Tanstack Router in React (Complete Tutorial)
This clip from the 'Tanstack Router in React (Complete Tutorial)' by Cosden Solutions shows how to create a dynamic route and use its parameters for data fetching.
Watch from 18:37 to 23:08. Focus on three key steps: Creating a dynamic route file with the dollar sign syntax (e.g., $postId.tsx). Defining a loader that receives a params object. Accessing the loader's data in the component with useLoaderData.
Let's solidify this with a code example. Imagine you have a route at src/routes/posts/$postId.tsx:
// src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
// Assume you have a server function `fetchPostById`
import { fetchPostById } from '../../lib/server/posts'
export const Route = createFileRoute('/posts/$postId')({
// The loader receives an object containing `params`.
loader: async ({ params }) => {
// The `postId` is available and typed as a string.
const { postId } = params
const post = await fetchPostById(postId)
return post
},
component: PostPage,
})
function PostPage() {
// The type of `post` is inferred from the loader's return type.
const post = Route.useLoaderData()
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
)
}
The loader has access to the params object, allowing you to fetch precisely the data needed for that specific route.
4. Context: Comparing Data Fetching Strategies
The loader function is powerful, but it's not the only way to fetch data in TanStack Start. It's important to understand the trade-offs. By default, a loader blocks rendering until the data is fetched. This is great for SEO but can lead to a longer wait for the initial page view.
This video provides an excellent summary of the different strategies you can employ.
The 3 ways to fetch data in TanStack start
Web Dev Cody's video, 'The 3 ways to fetch data in TanStack start', compares the blocking loader pattern with more modern, non-blocking approaches using React Suspense.
Watch the sections covering the three main approaches: Server-side data fetching with loader (00:23 - 02:13): This is the pattern we've focused on. Note the comment about it being 'the worst user experience' in some cases due to the wait time. Using Suspense and useSuspenseQuery (02:13 - 04:01): This shows how to render the page instantly and stream in data, improving perceived performance. Summary (04:56 - 05:41): This recap helps solidify the trade-offs.
To summarize the trade-offs:
| Method | Pro | Con |
|---|---|---|
Blocking loader |
Excellent for SEO; no layout shifts; data is present on render. | User waits for the slowest data fetch before seeing anything. |
Suspense + useSuspenseQuery |
Fast initial page load; streams data as it becomes available. | Can cause layout shifts; more complex UI state to manage. |
Client-side useQuery |
Familiar pattern from SPAs; fine-grained control over loading UI. | Bad for SEO; content is not in the initial HTML. |
For your goal of migrating an SPA to SSR, starting with the blocking loader pattern is the most direct way to leverage server-side capabilities. You can then progressively enhance the user experience with Suspense where needed.
Conclusion
Today you've learned the fundamental pattern for server-side data fetching in TanStack Start. You now know how to create a loader function, secure your API calls with a createServerFn, handle dynamic route parameters, and access the typed data in your components.
Key Takeaways:
- The
loaderfunction is the primary mechanism for fetching data before a route renders, running on the server for SSR and the client for navigation. - Combining
loaderwithcreateServerFnis the standard pattern for securely fetching data from protected APIs. - TanStack Router provides end-to-end type safety, inferring the data type from the
loader's return value through to theuseLoaderDatahook. - Loaders can access route
paramsto fetch data specific to a dynamic route (e.g.,/posts/$postId). - You must choose a fetching strategy (blocking loader vs. streaming with Suspense) based on the specific needs of each page, balancing SEO and perceived performance.
Next Lesson Preview:
What happens when a data fetch fails or takes a long time? In our next lesson, we will address this by learning how to "Handle loader errors and implement pending states for route transitions." You'll learn how to use the built-in errorComponent and pendingComponent options in your route definitions to create a more resilient and user-friendly experience.