Introduction to Performance Optimization and Common Pitfalls
Welcome to Chapter 16! Throughout our journey, we’ve built robust applications using the TanStack libraries. Now, it’s time to elevate our skills by focusing on two critical aspects of professional development: performance optimization and avoiding common pitfalls. Building features is one thing; building fast, stable, and maintainable features is another.
In this chapter, we’ll dive deep into strategies for making your TanStack applications snappy and responsive. We’ll explore how to leverage the built-in optimization features of TanStack Query, Table, Router, and Virtual, alongside general React best practices. More importantly, we’ll identify common mistakes that developers often make and equip you with the knowledge to troubleshoot and prevent them. Get ready to refine your understanding and build truly high-performing applications!
This chapter assumes you’re comfortable with the core concepts of TanStack Query, Table, Router, and Virtual as covered in previous chapters, along with fundamental React principles like state management, props, and hooks. We’ll be building upon that foundation to fine-tune our applications.
Core Concepts: Building Blazing Fast Applications
Optimizing performance isn’t a single step; it’s a mindset. It involves understanding how each part of your application works and identifying bottlenecks. The TanStack libraries are designed with performance in mind, offering powerful tools to help you.
TanStack Query: Smart Data Management
TanStack Query (version 5 as of early 2026) is your best friend for managing server state efficiently. Its caching mechanisms are incredibly powerful, but understanding how to configure them is key to optimal performance.
Stale-While-Revalidate Strategy
Remember TanStack Query’s default “stale-while-revalidate” strategy? It means data is shown immediately (if cached), and then a fresh fetch happens in the background. This is a huge win for perceived performance.
Let’s look at staleTime and cacheTime again, but this time with a performance lens.
staleTime: This is the duration until a query’s data becomes “stale.” Once stale, the next time the query is observed, it will refetch in the background. If you setstaleTimetoInfinity, the data will never automatically refetch unless manually invalidated. This is useful for data that changes very infrequently or when you only want to refetch on specific user actions.cacheTime: This is how long inactive query data remains in the cache before being garbage collected. By default, it’s 5 minutes. If a user navigates away from a component using a query, that query becomes inactive. If they return withincacheTime, the data is still there, ready to be displayed instantly. IfcacheTimeis too low, you might lose data prematurely, leading to more network requests. If too high, you might hold onto too much memory for data that’s unlikely to be revisited.
Why does this matter for performance?
By setting an appropriate staleTime, you can reduce unnecessary network requests. If data changes every minute, staleTime: 5000 (5 seconds) might be good. If it changes once a day, staleTime: 1000 * 60 * 60 * 24 (24 hours) is perfect!
Let’s consider an example where we fetch a list of static categories.
// src/hooks/useCategories.ts
import { useQuery } from '@tanstack/react-query';
interface Category {
id: string;
name: string;
}
async function fetchCategories(): Promise<Category[]> {
const response = await fetch('/api/categories');
if (!response.ok) {
throw new Error('Failed to fetch categories');
}
return response.json();
}
export function useCategories() {
return useQuery<Category[]>({
queryKey: ['categories'],
queryFn: fetchCategories,
staleTime: 1000 * 60 * 60, // Data becomes stale after 1 hour
cacheTime: 1000 * 60 * 60 * 24, // Keep in cache for 24 hours even if inactive
});
}
Explanation:
- We’re setting
staleTimeto 1 hour. This means if a user views the categories, navigates away, and comes back within an hour, they will see the cached data instantly, and no background refetch will occur. After an hour, the next time they view it, a background refetch will happen. cacheTimeis set to 24 hours. If the user doesn’t visit any component usinguseCategoriesfor 24 hours, the data will be garbage collected from the cache. This balances responsiveness with memory usage.
Data Selection and Transformation (select)
Fetching all data and then displaying only a subset can be inefficient if the full dataset is very large or if you need to perform complex transformations. TanStack Query’s select option allows you to transform or pick specific parts of the data after it’s fetched but before it’s returned by useQuery. This can prevent unnecessary re-renders in components that only care about a small slice of the data.
// src/hooks/useProductNames.ts
import { useQuery } from '@tanstack/react-query';
interface Product {
id: string;
name: string;
price: number;
description: string;
}
async function fetchProducts(): Promise<Product[]> {
const response = await fetch('/api/products');
if (!response.ok) {
throw new Error('Failed to fetch products');
}
return response.json();
}
export function useProductNames() {
return useQuery<Product[], Error, string[]>({
queryKey: ['products'],
queryFn: fetchProducts,
select: (products) => products.map(product => product.name), // Only select product names
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
// In a component:
function ProductList() {
const { data: productNames, isLoading } = useProductNames();
if (isLoading) return <div>Loading product names...</div>;
return (
<ul>
{productNames?.map(name => <li key={name}>{name}</li>)}
</ul>
);
}
Explanation:
- The
useProductNameshook fetches the fullProduct[]array. - The
selectfunction then transforms this array intostring[]containing only the names. - The
ProductListcomponent only receives an array of strings, reducing its rendering complexity and dependencies. If any other part of the product data changes (e.g., price or description),ProductListwon’t re-render unless the names themselves change.
Query Invalidation and Refetching
While automatic refetching is great, sometimes you need to explicitly tell TanStack Query that data has changed on the server. This is where queryClient.invalidateQueries comes in. Invalidating queries ensures your UI shows the most up-to-date data after a mutation (e.g., creating, updating, or deleting an item).
Why it’s important: Without proper invalidation, users might see stale data after performing an action, leading to a poor user experience.
// src/components/NewProductForm.tsx
import React, { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface NewProduct {
name: string;
price: number;
}
async function createProduct(newProduct: NewProduct) {
const response = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newProduct),
});
if (!response.ok) {
throw new Error('Failed to create product');
}
return response.json();
}
function NewProductForm() {
const queryClient = useQueryClient();
const [name, setName] = useState('');
const [price, setPrice] = useState(0);
const mutation = useMutation({
mutationFn: createProduct,
onSuccess: () => {
// Invalidate the 'products' query to refetch the list after a successful creation
queryClient.invalidateQueries({ queryKey: ['products'] });
setName('');
setPrice(0);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({ name, price });
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Product Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="number"
placeholder="Price"
value={price}
onChange={(e) => setPrice(parseFloat(e.target.value))}
/>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create Product'}
</button>
{mutation.isError && <div>Error: {mutation.error.message}</div>}
</form>
);
}
Explanation:
- After
createProductsuccessfully adds a new product,queryClient.invalidateQueries({ queryKey: ['products'] })is called. - This marks any active or inactive queries with the
['products']key as stale. - If a component is currently observing the
['products']query, it will immediately refetch the data, updating the UI to show the new product.
TanStack Table & Virtual: Handling Large Datasets
When dealing with hundreds or thousands of rows, performance can quickly degrade. TanStack Table (v8) and TanStack Virtual (v3) are designed to tackle this head-on.
Memoization for Table Columns and Data
The columns and data props passed to useReactTable are often objects or arrays. If these are recreated on every render, it can trigger unnecessary re-renders of the table, even if the underlying data hasn’t changed. Using React’s useMemo hook is crucial here.
// src/components/OptimizedProductTable.tsx
import React, { useMemo } from 'react';
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
import { useProducts } from '../hooks/useProducts'; // Assume this fetches Product[]
interface Product {
id: string;
name: string;
price: number;
description: string;
}
const columnHelper = createColumnHelper<Product>();
function OptimizedProductTable() {
const { data: products, isLoading } = useProducts();
// Memoize columns definition
const columns = useMemo(
() => [
columnHelper.accessor('name', {
header: 'Product Name',
cell: (info) => info.getValue(),
}),
columnHelper.accessor('price', {
header: 'Price',
cell: (info) => `$${info.getValue().toFixed(2)}`,
}),
columnHelper.accessor('description', {
header: 'Description',
cell: (info) => info.getValue(),
}),
],
[] // Empty dependency array means columns are defined once
);
// Memoize data if it's derived or transformed in any way
// In this case, useProducts already returns memoized data from TanStack Query cache,
// but if you were doing local filtering/sorting, you'd memoize here.
const tableData = useMemo(() => products || [], [products]);
const table = useReactTable({
data: tableData,
columns,
getCoreRowModel: getCoreRowModel(),
});
if (isLoading) return <div>Loading products...</div>;
return (
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
Explanation:
columnsare wrapped inuseMemowith an empty dependency array[]. This ensures the column definitions are created only once and don’t cause the table to re-render unnecessarily if other state inOptimizedProductTablechanges.tableDatais also wrapped inuseMemo. WhileproductsfromuseProductsis already stable due to TanStack Query’s caching, if you perform any local filtering, sorting, or transformation before passing to the table,useMemois vital to prevent re-computation and re-renders.
Virtualization with TanStack Virtual
For truly massive lists and tables, displaying every single row is a performance killer. TanStack Virtual is a headless library that renders only the items currently visible in the viewport, significantly reducing DOM elements and improving scroll performance.
Concept: Instead of rendering 10,000 rows, TanStack Virtual might render 20-50 rows at a time, plus a few buffer rows, and dynamically update them as the user scrolls.
Explanation of the diagram:
- When the
Userscrolls,TanStack Virtualintercepts this. - It calculates which items should be visible based on scroll position and container size.
- It then provides a
virtualItemsarray to yourReact Component. - Your
React Componentrenders only thesevirtualItemsto theDOM. - This ensures the
Usersees a smooth scrolling experience without the browser rendering thousands of elements.
Integrating TanStack Virtual with TanStack Table is a common pattern for high-performance data grids. You typically use useVirtualizer (or useVirtualizer from the React adapter) to manage the virtualized rows.
// src/components/VirtualizedProductTable.tsx (Simplified example)
import React, { useMemo, useRef } from 'react';
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
import { useVirtualizer } from '@tanstack/react-virtual'; // v3
import { useProducts } from '../hooks/useProducts';
interface Product {
id: string;
name: string;
price: number;
description: string;
}
const columnHelper = createColumnHelper<Product>();
function VirtualizedProductTable() {
const { data: products, isLoading } = useProducts();
const parentRef = useRef<HTMLDivElement>(null); // Ref for the scrollable container
const columns = useMemo(
() => [
columnHelper.accessor('name', { header: 'Product Name' }),
columnHelper.accessor('price', { header: 'Price' }),
columnHelper.accessor('description', { header: 'Description' }),
],
[]
);
const tableData = useMemo(() => products || [], [products]);
const table = useReactTable({
data: tableData,
columns,
getCoreRowModel: getCoreRowModel(),
});
const rows = table.getRowModel().rows; // Get all rows from TanStack Table
// TanStack Virtualizer for rows
const rowVirtualizer = useVirtualizer({
count: rows.length, // Total number of items
getScrollElement: () => parentRef.current, // The scrollable element
estimateSize: () => 35, // Estimated row height (important for initial scroll)
overscan: 5, // Render 5 extra rows above/below visible area for smooth scrolling
});
if (isLoading) return <div>Loading products...</div>;
const virtualRows = rowVirtualizer.getVirtualItems();
return (
<div
ref={parentRef}
style={{
height: '400px', // Fixed height for scrollable container
overflow: 'auto', // Make it scrollable
border: '1px solid #ccc',
}}
>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
style={{ padding: '8px', borderBottom: '1px solid #eee', textAlign: 'left' }}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody
style={{
height: rowVirtualizer.getTotalSize(), // Set total height for scrollbar
position: 'relative',
}}
>
{virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index]; // Get the actual row data from TanStack Table
return (
<tr
key={row.id}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement} // Crucial for dynamic row heights
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`, // Position the row
}}
>
{row.getVisibleCells().map((cell) => (
<td
key={cell.id}
style={{ padding: '8px', borderBottom: '1px solid #eee', textAlign: 'left' }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
);
})}
</tbody>
</table>
</div>
);
}
Explanation:
- We create a
parentRefto denote the scrollable container. useVirtualizeris initialized withcount(total rows),getScrollElement,estimateSize(a good guess for initial rendering), andoverscan(for smoother scrolling).virtualRowscontains only the items that should be rendered.- The
tbodyheight is set torowVirtualizer.getTotalSize()to create a scrollbar that reflects the total number of rows. - Each virtualized
tris absolutely positioned usingtransform: translateYto place it correctly within the scrollable area, and itsrefis passed torowVirtualizer.measureElementto allow TanStack Virtual to precisely calculate its actual height. This is a powerful pattern for handling large datasets.
TanStack Router: Efficient Navigation and Data Loading
TanStack Router (v1) is designed to be highly performant, especially with its loader-first approach to data fetching.
Route Loaders and Data Fetching Waterfalls
TanStack Router encourages fetching data before a route component renders using loader functions. This prevents “data fetching waterfalls,” where a parent component fetches data, then a child component fetches more data, and so on, leading to sequential, slow loading.
How it helps: By defining loaders at the route level, all necessary data for a route (and its children) can be fetched in parallel as the user navigates.
Explanation of the diagram:
- The
Userclicks a link. Routeridentifies the targetRoute.- Instead of rendering immediately and then fetching, it executes all
Parent & Child Loaders in Parallel. - Once
All Data Ready, theRoute Componentis rendered. This ensures a faster initial render with all data present.
Code Splitting with lazy Loaders
For larger applications, loading all route components at once can increase initial bundle size and load times. TanStack Router supports lazy loading for route components, allowing you to split your application into smaller chunks that are loaded only when needed. This utilizes React’s lazy and Suspense features.
// src/routes/index.tsx (Example)
import { Route, lazyRouteComponent } from '@tanstack/react-router';
import { rootRoute } from './__root'; // Assuming you have a root route
export const indexRoute = new Route({
getParentRoute: () => rootRoute,
path: '/',
// Use lazyRouteComponent for code splitting
component: lazyRouteComponent(() => import('../components/HomePage')),
});
export const aboutRoute = new Route({
getParentRoute: () => rootRoute,
path: '/about',
component: lazyRouteComponent(() => import('../components/AboutPage')),
});
export const productsRoute = new Route({
getParentRoute: () => rootRoute,
path: '/products',
component: lazyRouteComponent(() => import('../components/ProductsPage')),
loader: async () => {
// This loader will run before the component is loaded
// and can prefetch data needed by ProductsPage
console.log('Fetching products data for products page...');
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate fetch
return { products: [{ id: '1', name: 'Lazy Product' }] };
},
});
Explanation:
lazyRouteComponent(() => import('../components/HomePage'))tells the router to dynamically importHomePageonly when the/route is activated.- This creates separate JavaScript bundles for each lazily loaded component, reducing the initial load time of your application.
- The
loaderforproductsRoutestill runs eagerly, ensuring data is ready even before the component’s code chunk is fully loaded.
General React Performance Best Practices
Beyond specific TanStack features, core React optimization techniques remain vital.
React.memo: Memoizes components to prevent re-renders if their props haven’t changed.const MyMemoizedComponent = React.memo(({ data }) => { // This component will only re-render if 'data' prop changes return <div>{data.name}</div>; });useCallback: Memoizes functions to prevent unnecessary re-creation on every render, which is crucial when passing functions as props to memoized child components.const handleClick = useCallback(() => { console.log('Button clicked'); }, []); // Empty dependency array means this function is created onceuseMemo: Memoizes values to prevent expensive re-calculations on every render.const expensiveValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);- Key Props: Always provide unique
keyprops for lists of elements to help React efficiently update the DOM.
Step-by-Step Implementation: Optimizing a Data-Driven Component
Let’s take a simple component that displays product details and apply some of the optimization techniques we’ve discussed.
Scenario: We have a component that fetches a single product by ID and displays its details. We want to optimize its data fetching and rendering.
1. Initial (Unoptimized) Component:
Let’s assume useProductDetails fetches the full product object.
// src/hooks/useProductDetails.ts (Assume this exists)
import { useQuery } from '@tanstack/react-query';
interface Product {
id: string;
name: string;
price: number;
description: string;
// ... many other fields
}
async function fetchProductById(productId: string): Promise<Product> {
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) {
throw new Error('Failed to fetch product');
}
return response.json();
}
export function useProductDetails(productId: string) {
return useQuery<Product>({
queryKey: ['product', productId],
queryFn: () => fetchProductById(productId),
enabled: !!productId,
});
}
// src/components/ProductDetailCard.tsx (Initial version)
import React from 'react';
import { useProductDetails } from '../hooks/useProductDetails';
interface ProductDetailCardProps {
productId: string;
}
function ProductDetailCard({ productId }: ProductDetailCardProps) {
const { data: product, isLoading, isError, error } = useProductDetails(productId);
if (isLoading) return <div>Loading product details...</div>;
if (isError) return <div>Error: {error?.message}</div>;
if (!product) return <div>No product found.</div>;
return (
<div style={{ border: '1px solid #ddd', padding: '15px', borderRadius: '8px' }}>
<h3>{product.name}</h3>
<p>Price: ${product.price.toFixed(2)}</p>
<p>Description: {product.description}</p>
{/* ... potentially many other fields */}
</div>
);
}
2. Optimizing with staleTime and select:
We realize product details don’t change very often, and sometimes we only need a few fields. Let’s adjust useProductDetails to include staleTime and create a specialized hook for just the name and price using select.
First, update src/hooks/useProductDetails.ts:
// src/hooks/useProductDetails.ts (Optimized with staleTime)
import { useQuery } from '@tanstack/react-query';
interface Product {
id: string;
name: string;
price: number;
description: string;
// ... many other fields
}
async function fetchProductById(productId: string): Promise<Product> {
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) {
throw new Error('Failed to fetch product');
}
return response.json();
}
export function useProductDetails(productId: string) {
return useQuery<Product>({
queryKey: ['product', productId],
queryFn: () => fetchProductById(productId),
enabled: !!productId,
staleTime: 1000 * 60 * 5, // Keep data fresh for 5 minutes
cacheTime: 1000 * 60 * 30, // Keep in cache for 30 minutes
});
}
// src/hooks/useProductNameAndPrice.ts (NEW file)
import { useQuery } from '@tanstack/react-query';
import { fetchProductById } from './useProductDetails'; // Re-use the fetcher
interface ProductNameAndPrice {
name: string;
price: number;
}
export function useProductNameAndPrice(productId: string) {
return useQuery<Product, Error, ProductNameAndPrice>({
queryKey: ['product', productId], // Same query key, so it shares cache with useProductDetails
queryFn: () => fetchProductById(productId),
enabled: !!productId,
staleTime: 1000 * 60 * 5,
cacheTime: 1000 * 60 * 30,
select: (product) => ({ name: product.name, price: product.price }), // Select only name and price
});
}
Explanation:
- In
useProductDetails, we addedstaleTimeandcacheTimeto manage how often the data is refetched and how long it stays in memory. - We created a new hook,
useProductNameAndPrice, that uses the samequeryKeyandqueryFnbut leverages theselectoption to return only thenameandprice. This means ifuseProductDetailshas already fetched the full product,useProductNameAndPricewill use that cached data and simply select the required fields without another network request. It also ensures that components using this hook only re-render ifnameorpricechange, not if other product fields change.
Now, let’s create a component that uses this optimized useProductNameAndPrice hook.
// src/components/ProductSummaryCard.tsx (NEW file)
import React from 'react';
import { useProductNameAndPrice } from '../hooks/useProductNameAndPrice';
interface ProductSummaryCardProps {
productId: string;
}
// We can also memoize this component if its parent re-renders frequently
const ProductSummaryCard = React.memo(({ productId }: ProductSummaryCardProps) => {
const { data: productSummary, isLoading, isError, error } = useProductNameAndPrice(productId);
if (isLoading) return <div>Loading product summary...</div>;
if (isError) return <div>Error: {error?.message}</div>;
if (!productSummary) return <div>No product summary found.</div>;
return (
<div style={{ border: '1px solid #eee', padding: '10px', borderRadius: '5px', marginBottom: '10px' }}>
<h4>{productSummary.name}</h4>
<p>Price: ${productSummary.price.toFixed(2)}</p>
</div>
);
});
export default ProductSummaryCard; // Export as default for easier lazy loading if desired
Explanation:
ProductSummaryCardnow usesuseProductNameAndPrice, ensuring it only receives and re-renders based on the name and price.- We’ve also wrapped
ProductSummaryCardinReact.memo. This is a general React optimization that tells React not to re-render this component if itsproductIdprop hasn’t changed.
Mini-Challenge: Optimize a User List Component
Challenge: You have a component that displays a list of users. Currently, it fetches all user data, but in a specific part of your application, you only need to show their id and name. Optimize this scenario.
- Create a
useUsershook that fetches an array of user objects (each withid,name,email,role, etc.). Set a reasonablestaleTime(e.g., 1 minute) andcacheTime(e.g., 10 minutes). - Create a
useUserNameshook that uses the same query key asuseUsersbut leverages theselectoption to return only an array of objects containingidandname. - Create a
UserListDisplaycomponent that usesuseUserNamesto display only the user IDs and names. Wrap this component inReact.memo.
Hint: Remember that select functions allow you to transform the data after it’s fetched. The queryKey is crucial for cache sharing.
What to observe/learn:
- How
staleTimeandcacheTimeaffect subsequent fetches. - How
selectcan reduce re-renders and provide focused data to components. - The benefit of
React.memowhen props are stable. - The power of TanStack Query’s cache sharing with identical
queryKeys.
Common Pitfalls & Troubleshooting
Even with powerful tools, it’s easy to stumble. Knowing common pitfalls and how to troubleshoot them will save you hours.
1. Unnecessary Re-renders
This is perhaps the most common performance issue in React applications.
- Pitfall: Components re-rendering when their props or state haven’t meaningfully changed. This often happens because objects or arrays passed as props are new references on every render, even if their contents are the same.
- Troubleshooting:
- React DevTools Profiler: Use the React DevTools to profile your application. Look for components that re-render frequently without a clear reason.
console.loganduseEffect: Temporarily addconsole.log('Component re-rendered', props)inside your component or useuseEffect(() => { console.log('Props changed', props); }, [props]);to see what props are actually causing a re-render.- Solutions:
React.memofor functional components.useCallbackfor memoizing functions passed as props.useMemofor memoizing expensive values or objects/arrays passed as props.- Ensure your
useStateupdates only change the necessary parts of the state.
2. TanStack Query Cache Invalidation Issues
- Pitfall: Stale data appearing in the UI after a mutation, or excessive refetching.
- Stale Data: Forgetting to invalidate queries after a mutation, leading to the UI not reflecting the latest server state.
- Excessive Refetching: Invalidating too broadly (e.g.,
queryClient.invalidateQueries()) or settingstaleTimetoo low for static data.
- Troubleshooting:
- TanStack Query Devtools: The Devtools are invaluable! They show you query states (stale, fetching, inactive), cache times, and when queries are being invalidated/refetched.
- Network Tab: Observe network requests to see when and how often data is being fetched.
- Solutions:
- Targeted Invalidation: Use specific
queryKeys withqueryClient.invalidateQueries({ queryKey: ['your', 'key'] })orqueryClient.invalidateQueries({ queryKey: ['posts'], exact: true }). - Optimistic Updates: For a better UX, consider optimistic updates with
onMutateandonErrorcallbacks inuseMutation. - Adjust
staleTime: Fine-tunestaleTimebased on how frequently your data changes.
- Targeted Invalidation: Use specific
3. Data Fetching Waterfalls with TanStack Router
- Pitfall: Nested routes fetching data sequentially, leading to longer perceived load times.
- Troubleshooting:
- Network Tab: Look at the waterfall chart in your browser’s network tab. If you see requests happening one after another for data that could be fetched in parallel, you likely have a waterfall.
- React DevTools (Component Tree): Observe when components render relative to data availability.
- Solutions:
- Utilize Route Loaders: Define
loaderfunctions at the route level in TanStack Router. This ensures all data for a route hierarchy is fetched in parallel before the components render. - Loader Dependencies: Ensure child loaders depend on parent loader data using
getParentRoute, not by re-fetching.
- Utilize Route Loaders: Define
4. Poor Virtualization Configuration
- Pitfall: Jumpy scrolling, incorrect scrollbar size, or performance issues despite using a virtualization library.
- Troubleshooting:
- Visual Inspection: Does the scrollbar look correct? Does scrolling feel smooth?
estimateSizeAccuracy: If yourestimateSizeis far off from the actual item heights, the scrollbar can be inaccurate, and jumpiness might occur.measureElement(or equivalent) Missing: For dynamic heights, if you forget to pass themeasureElementref to your virtualized items, the virtualizer won’t know their true size.
- Solutions:
- Accurate
estimateSize: Provide the most accurate estimate of item size possible. - Dynamic Sizing: For variable-height items, ensure you are correctly using the virtualizer’s
measureElementormeasureSizecallback on each item. overscanAdjustment: Increaseoverscanif users report seeing blank areas while scrolling fast.
- Accurate
Summary
Phew! We’ve covered a lot of ground in optimizing our TanStack applications and avoiding common pitfalls. Here are the key takeaways:
- TanStack Query is your ally: Leverage
staleTime,cacheTime,select, and precisequeryClient.invalidateQueriesto manage server state efficiently, reduce network requests, and prevent unnecessary re-renders. - Master large datasets with Virtualization: Combine TanStack Table with TanStack Virtual to render only visible rows, drastically improving performance for massive lists and tables. Remember to memoize columns and data.
- Router for smooth navigation: Use TanStack Router’s
loaderfunctions to prevent data fetching waterfalls andlazyRouteComponentfor code splitting to reduce initial bundle size. - React fundamentals still matter: Don’t forget
React.memo,useCallback,useMemo, and properkeyprops for general component optimization. - DevTools are your best friends: The TanStack Query Devtools and React DevTools Profiler are indispensable for identifying performance bottlenecks and troubleshooting issues.
- Proactive Pitfall Avoidance: Understand common mistakes like unnecessary re-renders, incorrect cache invalidation, and data fetching waterfalls, and apply the learned strategies to prevent them.
By integrating these performance optimization techniques and being mindful of common pitfalls, you’re now equipped to build professional-grade, highly performant web applications with the TanStack ecosystem.
Next up, in Chapter 17, we’ll explore production best practices, including deployment strategies, monitoring, and maintaining your TanStack applications in the wild!
References
- TanStack Query Docs v5
- TanStack Table Docs v8
- TanStack Router Docs v1
- TanStack Virtual Docs v3
- React DevTools Documentation
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.