Welcome to Chapter 17! So far, we’ve built a solid understanding of the TanStack ecosystem, leveraging its powerful tools to manage state, build dynamic UIs, and handle complex data flows. We’ve created features, optimized performance, and made our applications interactive. But what happens when things go wrong? How do we ensure our code is reliable, and how do we get it into the hands of users efficiently?
This chapter is all about getting your TanStack-powered application ready for the real world. We’ll dive into the critical aspects of production readiness: implementing robust error handling to gracefully manage unexpected issues, strategizing effective testing to catch bugs before they reach users, and understanding modern deployment practices to get your application live. By the end of this chapter, you’ll have a comprehensive toolkit to build not just functional, but also resilient and deployable TanStack applications.
To get the most out of this chapter, it’s helpful to be familiar with the core concepts from previous chapters, especially those on TanStack Query for data fetching, TanStack Router for navigation, and general React component development. We’ll be building on that foundation to integrate these crucial production-grade practices. Let’s make our applications rock-solid!
Core Concepts: Building Resilient Applications
Building an application that works perfectly in your development environment is one thing; ensuring it performs flawlessly and handles unexpected scenarios gracefully in production is another. This section will explore the fundamental concepts of error handling, testing, and deployment within the TanStack ecosystem.
1. Robust Error Handling
Errors are an inevitable part of software development. How we anticipate, catch, and respond to them defines the robustness and user experience of our applications. TanStack libraries provide excellent mechanisms for managing errors, especially with asynchronous operations.
TanStack Query Error Management
TanStack Query (latest stable v5.x as of 2026-01-07) is designed with error handling in mind for its asynchronous data operations. When a query or mutation fails, TanStack Query provides immediate feedback and mechanisms to react.
- The
errorProperty: EveryuseQueryhook returns anerrorproperty. This property will benullif there’s no error, or it will contain the error object if the query failed. You can checkisErrororerrordirectly to conditionally render UI. onErrorCallbacks: You can defineonErrorcallbacks both at the individual query/mutation level and globally on theQueryClient. This is perfect for logging errors, showing toast notifications, or triggering side effects.useErrorBoundaryOption: For more severe errors that should halt rendering a component tree, TanStack Query offers auseErrorBoundaryoption. When set totrue, if a query errors, it will throw the error, allowing a React Error Boundary higher up in the component tree to catch it. This is a powerful pattern for isolating failures.retryLogic: TanStack Query automatically retries failed queries by default. You can configure the number of retries or disable them entirely, which is crucial for handling transient network issues.
TanStack Router Error Handling
TanStack Router (latest stable v1.x as of 2026-01-07) also provides mechanisms to handle errors that occur during route loading or rendering.
errorComponent: Each route definition can specify anerrorComponent. If any data loading (loaderfunction) or rendering within that route’s component tree throws an error, theerrorComponentfor that route (or a parent route) will be rendered instead. This allows you to show specific error messages for different parts of your application.- Global Error Handling: You can define a top-level
errorComponentin yourRouterinstance to catch errors not handled by more specific route error components, providing a consistent fallback for unexpected issues.
React Error Boundaries
At a foundational level, React (latest stable v19.x as of 2026-01-07) provides Error Boundaries. These are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of crashing the entire application. They are crucial for catching rendering errors that useQuery’s useErrorBoundary option propagates.
Let’s visualize how these error handling mechanisms can work together:
Figure 17.1: Integrated Error Handling Flow in a TanStack Application
This diagram illustrates how a user action might trigger an API request via useQuery. If that request fails, useQuery can either report the error directly to the component (M) or throw it (H) for a React Error Boundary (J) to catch. Similarly, if a loader function in TanStack Router fails during navigation (P), the Router can render a specific errorComponent (Q). The goal is to gracefully degrade, inform the user, and prevent a complete application crash.
2. Effective Testing Strategies
Testing is paramount for ensuring the quality and stability of your application. With the TanStack ecosystem, we often deal with asynchronous data, routing, and complex UI interactions, which require specific testing approaches.
Unit Testing TanStack Components and Hooks
- Custom Hooks with
useQuery: For custom hooks that wrapuseQuery, you’ll want to test their behavior in various states (loading, success, error). Tools like@testing-library/react-hooks(orrenderHookfrom@testing-library/reactfor newer versions) are invaluable here. You’ll often mock your API calls using a library like Mock Service Worker (MSW) or simple Jest mocks to control the data returned. - Individual Components: Test your presentational components in isolation, ensuring they render correctly based on props, including different states of TanStack Query data (e.g.,
isLoading,isError,data).
Integration Testing
- TanStack Router: Test navigation flows, ensuring routes load correctly,
loadersfetch data as expected, and URL parameters are handled properly. You might use@testing-library/reactto render your application with a test router and simulate user interactions. - TanStack Table/Form: Test complex interactions within your tables and forms. For tables, this means testing sorting, filtering, pagination, and row selection. For forms, it’s about validating input, submission, and error display. Again, MSW can simulate backend responses for form submissions.
Mocking API Calls with MSW
Mock Service Worker (MSW) is a powerful tool for intercepting network requests at the service worker level (in browsers) or Node.js level (in tests). This allows you to define mock responses for your API endpoints, making your tests fast, reliable, and independent of an actual backend server. It’s highly recommended for testing useQuery and useMutation hooks.
3. Deployment Best Practices
Once your application is robust and well-tested, the final step is to deploy it. Modern frontend deployment involves considerations like build processes, hosting, and performance optimizations.
- Build Process: Use your framework’s build tools (e.g., Vite, Next.js, Remix) to compile your TypeScript/JavaScript, optimize assets (CSS, images), and generate a production-ready bundle. TanStack libraries are designed to be tree-shakable, meaning unused code is removed during the build, resulting in smaller bundles.
- Client-Side Hydration (for SSR/SSG): If you’re using a framework like TanStack Start (which builds on Vite and offers SSR/SSG capabilities), you’ll leverage client-side hydration. This is where the server renders the initial HTML, and then the client-side JavaScript “takes over” and makes the page interactive. TanStack Query’s
dehydrate/hydratefunctions are crucial here for transferring server-fetched data to the client without refetching. - Environment Variables: Manage sensitive information (API keys, backend URLs) using environment variables. Your build process should correctly inject these into your application based on the deployment environment (development, staging, production).
- Performance Optimizations:
- Code Splitting: Break your application’s JavaScript bundle into smaller chunks that are loaded on demand. TanStack Router naturally supports code splitting for routes.
- Caching: Configure proper HTTP caching headers for your static assets. TanStack Query handles its own data caching, but browser caching for your application’s code is also important.
- CDN (Content Delivery Network): Host your static assets on a CDN for faster delivery to users worldwide.
- Monitoring and Logging: Integrate tools to monitor your application’s performance, user behavior, and error rates in production. Services like Sentry for error tracking, or application performance monitoring (APM) tools, are essential. Your
onErrorcallbacks in TanStack Query are perfect places to send errors to these services.
Step-by-Step Implementation: Production-Ready Features
Let’s put these concepts into practice. We’ll add error handling to a useQuery hook and demonstrate a basic test setup.
Step 1: Enhancing Error Handling in TanStack Query
Imagine you have a component that fetches a list of products. We’ll enhance its error handling.
First, let’s set up a simple QueryClient and a component.
// src/queryClient.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Default options for all queries
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60, // 1 hour
retry: 3, // Retry failed queries 3 times
},
},
});
This queryClient.ts defines our QueryClient with some sensible defaults, including retries.
Now, let’s create a product fetching component.
// src/api/products.ts
export interface Product {
id: string;
name: string;
price: number;
}
// Simulate an API call that might fail
const fetchProducts = async (): Promise<Product[]> => {
const shouldFail = Math.random() > 0.7; // 30% chance of failure
if (shouldFail) {
throw new Error("Failed to fetch products! Network issue or server error.");
}
return new Promise((resolve) =>
setTimeout(() => {
resolve([
{ id: "1", name: "Laptop", price: 1200 },
{ id: "2", name: "Mouse", price: 25 },
{ id: "3", name: "Keyboard", price: 75 },
]);
}, 500)
);
};
export { fetchProducts };
This fetchProducts function simulates an API call, with a random chance of failure.
Now, in your component:
// src/components/ProductList.tsx
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { fetchProducts, Product } from '../api/products';
function ProductList() {
const { data, isLoading, isError, error } = useQuery<Product[], Error>({
queryKey: ['products'],
queryFn: fetchProducts,
// Add an onError callback for specific logging or side effects
onError: (err) => {
console.error("ProductList: Error fetching products:", err.message);
// Here you might send this error to an error tracking service like Sentry
},
// Setting useErrorBoundary to true will make this query throw its error
// which can then be caught by a React Error Boundary higher up the tree.
// useErrorBoundary: true,
});
if (isLoading) {
return <p>Loading products...</p>;
}
if (isError) {
// If useErrorBoundary is false (default), we handle the error here.
return <p style={{ color: 'red' }}>Error: {error?.message}</p>;
}
return (
<div>
<h2>Our Products</h2>
<ul>
{data?.map((product) => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
</div>
);
}
export default ProductList;
Explanation:
- We’ve imported
useQueryand ourfetchProductsfunction. - The
useQueryhook now explicitly types the potential error asError. - We added an
onErrorcallback. This callback fires after all retries have failed. It’s a great place to log errors to the console or send them to an external error tracking service. - The
isErroranderrorproperties are used to conditionally render an error message to the user. - The
useErrorBoundary: trueoption (currently commented out) demonstrates how you would tell TanStack Query to throw the error, allowing a React Error Boundary to catch it.
Step 2: Implementing a React Error Boundary
To gracefully catch errors thrown by components (or by useQuery when useErrorBoundary: true), we use a React Error Boundary.
// src/components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children?: ReactNode;
fallback?: ReactNode; // Optional fallback UI
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
error: null,
};
// This static method is called when an error is thrown. It returns an object
// to update the state, indicating an error has occurred.
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
// This method is called after an error has been caught. It's used for
// logging error information.
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Uncaught error:", error, errorInfo);
// Here you would send error to a logging service (e.g., Sentry.captureException(error, { extra: errorInfo }))
}
public render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return this.props.fallback || (
<div style={{ padding: '20px', border: '1px solid red', color: 'red' }}>
<h1>Something went wrong!</h1>
<p>We're sorry for the inconvenience. Please try refreshing the page.</p>
{this.state.error && <p>Details: {this.state.error.message}</p>}
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Explanation:
- This is a standard React class component that implements
getDerivedStateFromErrorto update its state when an error is thrown by a child. componentDidCatchis used for side effects like logging the error.- When
hasErroristrue, it renders a fallback UI instead of its children. - You can wrap any part of your component tree with this
ErrorBoundary.
Now, let’s wrap our ProductList component with this ErrorBoundary in App.tsx:
// src/App.tsx
import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; // v5.x
import { queryClient } from './queryClient';
import ProductList from './components/ProductList';
import ErrorBoundary from './components/ErrorBoundary'; // Import our ErrorBoundary
function App() {
return (
<QueryClientProvider client={queryClient}>
<div style={{ padding: '20px' }}>
<h1>TanStack Production Readiness Demo</h1>
<ErrorBoundary> {/* Wrap the potentially erroring component */}
<ProductList />
</ErrorBoundary>
{/* Example with useErrorBoundary: true
<ErrorBoundary fallback={<div>Failed to load critical data!</div>}>
<ProductListWithErrorBoundary /> // A version of ProductList with useErrorBoundary: true
</ErrorBoundary>
*/}
</div>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
export default App;
To see this in action:
- Run your application.
- Refresh the page multiple times. Because
fetchProductshas a 30% chance of failure, you should eventually see the error message rendered byProductList(ifuseErrorBoundaryisfalse) or theErrorBoundary’s fallback UI (ifuseErrorBoundaryistrueinProductList).
Step 3: Basic Testing with TanStack Query and MSW
Let’s write a simple test for our fetchProducts function and ProductList component using Jest and Mock Service Worker.
First, install necessary dependencies:
npm install --save-dev @testing-library/react @testing-library/jest-dom @tanstack/react-query-testing-library jest-environment-jsdom msw
# Or using yarn
yarn add --dev @testing-library/react @testing-library/jest-dom @tanstack/react-query-testing-library jest-environment-jsdom msw
Note: @tanstack/react-query-testing-library provides renderWithClient which simplifies testing useQuery hooks. For React 18+, renderHook from @testing-library/react is also a great option.
Configure MSW:
Create src/mocks/handlers.ts:
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'; // v2.x
export const handlers = [
http.get('/api/products', () => {
return HttpResponse.json([
{ id: "test-1", name: "Test Product A", price: 100 },
{ id: "test-2", name: "Test Product B", price: 200 },
], { status: 200 });
}),
http.get('/api/products-error', () => {
return HttpResponse.json({ message: "Internal Server Error" }, { status: 500 });
}),
];
Create src/mocks/server.ts:
// src/mocks/server.ts
import { setupServer } from 'msw/node'; // v2.x
import { handlers } from './handlers';
export const server = setupServer(...handlers);
Configure Jest to use MSW:
// setupTests.ts (or similar file configured in Jest setupFilesAfterEnv)
import '@testing-library/jest-dom';
import { server } from './src/mocks/server';
// Establish API mocking before all tests.
beforeAll(() => server.listen());
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers());
// Clean up after the tests are finished.
afterAll(() => server.close());
Make sure your ProductList uses the /api/products endpoint. Let’s modify fetchProducts to use a relative path, and then Jest with MSW will intercept it.
// src/api/products.ts (updated)
export interface Product {
id: string;
name: string;
price: number;
}
const fetchProducts = async (): Promise<Product[]> => {
// Use a relative path so MSW can intercept
const response = await fetch('/api/products');
if (!response.ok) {
throw new Error('Failed to fetch products');
}
return response.json();
};
export { fetchProducts };
Now, create src/components/ProductList.test.tsx:
// src/components/ProductList.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClientProvider } from '@tanstack/react-query';
import { QueryClient } from '@tanstack/react-query'; // Import QueryClient directly for tests
import ProductList from './ProductList';
import { server } from '../mocks/server'; // Import our MSW server
import { http, HttpResponse } from 'msw'; // v2.x
// Create a new QueryClient for each test to ensure isolation
const createTestQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
retry: false, // Disable retries in tests for faster feedback
},
},
});
describe('ProductList', () => {
it('renders loading state initially', () => {
// Render the component with a fresh QueryClient
render(
<QueryClientProvider client={createTestQueryClient()}>
<ProductList />
</QueryClientProvider>
);
expect(screen.getByText(/Loading products.../i)).toBeInTheDocument();
});
it('renders products on successful fetch', async () => {
render(
<QueryClientProvider client={createTestQueryClient()}>
<ProductList />
</QueryClientProvider>
);
// MSW will intercept the /api/products call and return mock data
await waitFor(() => {
expect(screen.getByText('Test Product A - $100')).toBeInTheDocument();
expect(screen.getByText('Test Product B - $200')).toBeInTheDocument();
});
expect(screen.queryByText(/Loading products.../i)).not.toBeInTheDocument();
});
it('renders error message on failed fetch', async () => {
// Override the default MSW handler for this specific test
server.use(
http.get('/api/products', () => {
return HttpResponse.json({ message: "Network Error" }, { status: 500 });
})
);
render(
<QueryClientProvider client={createTestQueryClient()}>
<ProductList />
</QueryClientProvider>
);
await waitFor(() => {
expect(screen.getByText(/Error: Failed to fetch products/i)).toBeInTheDocument();
});
expect(screen.queryByText(/Loading products.../i)).not.toBeInTheDocument();
});
});
Explanation:
- We import
render,screen, andwaitForfrom@testing-library/react. QueryClientProviderandQueryClientare used to provide the TanStack Query context to our component. We create acreateTestQueryClienthelper to ensure each test gets a fresh, isolatedQueryClient, and we disable retries for faster test execution.beforeAll,afterEach,afterAllhooks (fromsetupTests.ts) manage the MSW server lifecycle.- The first test verifies the
isLoadingstate. - The second test verifies successful data fetching. MSW intercepts the
/api/productscall and returns the mock data defined inhandlers.ts.waitForis used because the data fetching is asynchronous. - The third test demonstrates how to override a handler for a specific test case, simulating an API error.
This setup provides a robust foundation for testing your TanStack Query-driven components, ensuring your data fetching logic and UI reactions are correct.
Mini-Challenge: Global Error Handling for useQuery
You’ve seen how to handle errors locally with onError and how useErrorBoundary works. Now, let’s make error handling even more consistent.
Challenge: Implement a global onError handler for all useQuery and useMutation calls within your application. This global handler should log a generic message and then re-throw the error (or call a global error reporting service).
Hint: The QueryClient constructor accepts defaultOptions.queries.onError and defaultOptions.mutations.onError.
What to observe/learn: How to centralize error logic, reducing boilerplate and ensuring consistent behavior across your application. This is especially useful for integrating with external error reporting tools.
Common Pitfalls & Troubleshooting
Not Handling Loading/Error States Explicitly: A common mistake is assuming data will always be available. Always account for
isLoading,isError, andisSuccessstates in your UI. If you only renderdatawhendatais present, you might show a blank screen during loading or after an error.- Troubleshooting: Use conditional rendering or default values.
if (isLoading) return <Spinner />; if (isError) return <ErrorMessage error={error} />; return <DataComponent data={data} />;
- Troubleshooting: Use conditional rendering or default values.
Over-Mocking in Tests: While mocking is essential, over-mocking can make tests brittle. If you mock every single function call, your tests might pass even if the underlying implementation changes in a way that breaks real-world behavior.
- Troubleshooting: Use MSW for network requests (integration level) and only mock specific utility functions or external libraries (unit level). Focus on testing component behavior, not the internal workings of
useQueryitself.
- Troubleshooting: Use MSW for network requests (integration level) and only mock specific utility functions or external libraries (unit level). Focus on testing component behavior, not the internal workings of
Incorrect Environment Variable Configuration in Deployment: Forgetting to set environment variables or setting them incorrectly for a production build can lead to your application failing to connect to APIs or using incorrect settings.
- Troubleshooting: Double-check your CI/CD pipeline and hosting provider’s environment variable settings. Ensure your build process correctly injects these variables (e.g.,
VITE_APP_API_URLfor Vite,NEXT_PUBLIC_API_URLfor Next.js). Never commit sensitive keys directly to your repository.
- Troubleshooting: Double-check your CI/CD pipeline and hosting provider’s environment variable settings. Ensure your build process correctly injects these variables (e.g.,
Forgetting to Hydrate State in SSR/SSG: When using Server-Side Rendering or Static Site Generation with TanStack Query, if you don’t dehydrate the
QueryClientstate on the server and rehydrate it on the client, the client-side application will refetch all data, leading to a flash of loading states and slower perceived performance.- Troubleshooting: Ensure your SSR/SSG setup correctly uses
dehydrateon the server andhydrateon the client, passing the dehydrated state in the initial HTML response. TanStack Start handles this automatically for you.
- Troubleshooting: Ensure your SSR/SSG setup correctly uses
Summary
Congratulations on completing Chapter 17! You’ve taken crucial steps towards building production-ready TanStack applications. Here’s a quick recap of the key takeaways:
- Error Handling:
- TanStack Query provides
isError,error,onError, anduseErrorBoundaryfor robust asynchronous error management. - TanStack Router offers
errorComponentto gracefully handle routing and data loading failures. - React Error Boundaries are essential for catching rendering errors and providing fallback UIs.
- TanStack Query provides
- Testing:
- Use
@testing-library/reactandrenderHookfor unit testing components and custom hooks. - Mock Service Worker (MSW) is the gold standard for mocking API requests in tests, ensuring reliability and speed.
- Always aim for a balance between unit and integration tests.
- Use
- Deployment:
- Understand your build process and leverage optimizations like tree-shaking and code splitting.
- Properly manage environment variables for different deployment environments.
- Utilize client-side hydration for SSR/SSG to avoid unnecessary data refetches.
- Implement monitoring and logging solutions to track application health in production.
By diligently applying these practices, you’re not just writing code; you’re building reliable, maintainable, and user-friendly applications that can stand the test of time in a production environment.
What’s next? With a solid understanding of production readiness, you’re well-equipped to tackle even more advanced topics. The final chapters will likely delve into more sophisticated architectural patterns, performance deep-dives, or even explore how to extend TanStack libraries with custom plugins and integrations. Stay curious and keep building!
References
- TanStack Query Docs (v5)
- TanStack Router Docs (v1)
- React Docs: Error Boundaries (v19)
- Testing Library Docs
- Mock Service Worker (MSW) Docs (v2)
- Supastarter Dev Tips: Prefetching Queries in TanStack Query (2025)
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.