Documentation Index
Fetch the complete documentation index at: https://mintlify.com/TanStack/router/llms.txt
Use this file to discover all available pages before exploring further.
Data Fetching
TanStack Start provides powerful data fetching capabilities that integrate seamlessly with TanStack Router. Learn how to fetch data efficiently using server functions, loaders, and various loading strategies.
Overview
Data fetching in TanStack Start can happen:
- On the server during SSR
- On the client after hydration
- In route loaders before navigation
- In components on-demand
Using Route Loaders
Route loaders are the primary way to fetch data for a route:
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
const fetchPost = createServerFn({ method: 'GET' })
.inputValidator((postId: string) => postId)
.handler(async ({ data }) => {
const res = await fetch(`https://api.example.com/posts/${data}`)
return await res.json()
})
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params }) => fetchPost({ data: params.postId }),
component: PostComponent,
})
function PostComponent() {
const post = Route.useLoaderData()
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
)
}
Data Loading Strategies
Parallel Loading
Load multiple resources simultaneously:
const fetchPost = createServerFn({ method: 'GET' })
.inputValidator((id: string) => id)
.handler(async ({ data }) => {
return await db.posts.findById(data)
})
const fetchComments = createServerFn({ method: 'GET' })
.inputValidator((postId: string) => postId)
.handler(async ({ data }) => {
return await db.comments.findByPostId(data)
})
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
// Load in parallel
const [post, comments] = await Promise.all([
fetchPost({ data: params.postId }),
fetchComments({ data: params.postId }),
])
return { post, comments }
},
})
Deferred Loading
Defer slow data to improve perceived performance:
import { Await } from '@tanstack/react-router'
import { Suspense } from 'react'
const fastData = createServerFn({ method: 'GET' }).handler(async () => {
return await db.users.findCurrent()
})
const slowData = createServerFn({ method: 'GET' }).handler(async () => {
await new Promise((r) => setTimeout(r, 2000)) // Simulate slow query
return await db.analytics.getReport()
})
export const Route = createFileRoute('/dashboard')({
loader: async () => {
return {
user: await fastData(), // Await fast data
analytics: slowData(), // Don't await - defer to client
}
},
component: Dashboard,
})
function Dashboard() {
const { user, analytics } = Route.useLoaderData()
return (
<div>
<h1>Welcome, {user.name}</h1>
<Suspense fallback={<div>Loading analytics...</div>}>
<Await promise={analytics}>
{(data) => <AnalyticsChart data={data} />}
</Await>
</Suspense>
</div>
)
}
Waterfall Loading
Load data that depends on previous results:
export const Route = createFileRoute('/users/$userId/posts')({
loader: async ({ params }) => {
// Load user first
const user = await fetchUser({ data: params.userId })
// Then load posts based on user's preferences
const posts = await fetchUserPosts({
data: {
userId: user.id,
limit: user.preferences.postsPerPage,
},
})
return { user, posts }
},
})
Conditional Loading
Load data based on conditions:
export const Route = createFileRoute('/profile')({
loader: async ({ context }) => {
const isAuthenticated = await checkAuth()
if (!isAuthenticated) {
throw redirect({ to: '/login' })
}
const user = await fetchCurrentUser()
// Only fetch admin data if user is admin
const adminData = user.isAdmin ? await fetchAdminData() : null
return { user, adminData }
},
})
Fetching in Components
Fetch data on-demand within components:
import { useState } from 'react'
import { createServerFn } from '@tanstack/react-start'
const searchPosts = createServerFn({ method: 'GET' })
.inputValidator((query: string) => query)
.handler(async ({ data }) => {
return await db.posts.search(data)
})
function SearchComponent() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)
const handleSearch = async () => {
setLoading(true)
try {
const data = await searchPosts({ data: query })
setResults(data)
} finally {
setLoading(false)
}
}
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
/>
{loading && <div>Searching...</div>}
{results.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}
Caching Strategies
Client-Side Caching
Implement caching with React Query:
import { useQuery } from '@tanstack/react-query'
import { fetchPosts } from '~/utils/posts'
function PostsList() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: () => fetchPosts(),
staleTime: 5 * 60 * 1000, // 5 minutes
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Static Data Caching
Cache server function results statically:
import { createServerFn } from '@tanstack/react-start'
import { staticFunctionMiddleware } from '@tanstack/start-static-server-functions'
const getStaticData = createServerFn({ method: 'GET' })
.middleware([staticFunctionMiddleware])
.handler(async () => {
// This result will be cached at build time in production
return await db.settings.findAll()
})
Preloading Data
Preload data before navigation:
import { Link } from '@tanstack/react-router'
function PostLink({ postId }: { postId: string }) {
return (
<Link
to="/posts/$postId"
params={{ postId }}
preload="intent" // Preload on hover/focus
>
View Post
</Link>
)
}
Preload options:
preload="intent" - Preload on hover or focus
preload="render" - Preload when link renders
preload={false} - Disable preloading
Error Handling
Handling Errors in Loaders
import { notFound } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
try {
return await fetchPost({ data: params.postId })
} catch (error) {
if (error.status === 404) {
throw notFound()
}
throw error
}
},
errorComponent: ({ error }) => {
return <div>Error loading post: {error.message}</div>
},
notFoundComponent: () => {
return <div>Post not found</div>
},
})
Error Boundaries
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params }) => fetchPost({ data: params.postId }),
errorComponent: PostErrorComponent,
})
function PostErrorComponent({ error }: { error: Error }) {
return (
<div className="error">
<h2>Failed to load post</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>
Try Again
</button>
</div>
)
}
Revalidation
Invalidate Route Data
import { useRouter } from '@tanstack/react-router'
function PostEditor() {
const router = useRouter()
const handleSave = async (data: PostData) => {
await updatePost({ data })
// Revalidate the route data
await router.invalidate()
}
return <form onSubmit={handleSave}>...</form>
}
Revalidate on Actions
const updatePost = createServerFn({ method: 'POST' })
.inputValidator((data: PostInput) => data)
.handler(async ({ data }) => {
await db.posts.update(data)
return { success: true }
})
export const Route = createFileRoute('/posts/$postId/edit')({
loader: ({ params }) => fetchPost({ data: params.postId }),
component: EditPost,
})
function EditPost() {
const router = useRouter()
const post = Route.useLoaderData()
const handleSubmit = async (formData: FormData) => {
await updatePost({ data: Object.fromEntries(formData) })
await router.invalidate() // Revalidate all routes
router.navigate({ to: '/posts/$postId', params: { postId: post.id } })
}
return <form onSubmit={handleSubmit}>...</form>
}
Optimistic Updates
import { useState } from 'react'
import { useRouter } from '@tanstack/react-router'
function LikeButton({ postId, initialLikes }: Props) {
const [likes, setLikes] = useState(initialLikes)
const [isOptimistic, setIsOptimistic] = useState(false)
const router = useRouter()
const handleLike = async () => {
// Optimistic update
setLikes((prev) => prev + 1)
setIsOptimistic(true)
try {
await likePost({ data: postId })
await router.invalidate()
} catch (error) {
// Rollback on error
setLikes((prev) => prev - 1)
alert('Failed to like post')
} finally {
setIsOptimistic(false)
}
}
return (
<button onClick={handleLike} disabled={isOptimistic}>
{likes} Likes {isOptimistic && '(saving...)'}
</button>
)
}
Best Practices
-
Fetch Early
- Use route loaders to fetch data before rendering
- Leverage preloading for anticipated navigations
-
Parallel When Possible
- Load independent data in parallel with
Promise.all()
- Only use waterfalls when data dependencies exist
-
Defer Slow Operations
- Use deferred loading for non-critical data
- Show meaningful loading states
-
Handle Errors Gracefully
- Provide error boundaries for each route
- Give users clear error messages and recovery options
-
Cache Strategically
- Cache frequently accessed, rarely changing data
- Use stale-while-revalidate patterns
- Consider static generation for truly static data
-
Optimize Payload Size
- Only fetch the data you need
- Use database projections/selections
- Consider pagination for large datasets
-
Type Everything
- Define types for all data structures
- Use validators to ensure runtime type safety
- Leverage TypeScript’s inference
Next Steps