Testing Tanstack Start

Testing TanStack Start Components: A Practical Mocking Approach
When building knowledgegap.guide, an AI-assisted learning platform, I chose a modern stack: TanStack Start for the full-stack framework, TanStack Query for data fetching, tRPC for type-safe APIs, and Better-Auth for authentication. This combination is fantastic for development, but testing components turned into a puzzle with all these pieces interconnected.
Since there’s no official testing guidance for TanStack Start yet, I figured out an approach that works well for me. Fair warning: this probably doesn’t follow all testing best practices and definitely doesn’t implement the official TanStack Query testing guidance. But it’s straightforward, easy to write, and gets the job done. If you know better ways to test this stack, I’d love to hear from you—hit me up via email or on X @thasmin and I’ll update this post.
The Challenge: Complex Dependencies
TanStack Start components typically depend on several interconnected systems:
- TanStack Router for file-based routing and navigation
- TanStack Query for data fetching and caching
- tRPC for type-safe API calls
- Server-side loaders that run before component rendering
- Route-specific hooks like
useLoaderData()
anduseSearch()
Testing these components requires carefully mocking each dependency while maintaining the component’s behavior.
Mocking TanStack Router
TanStack Start uses file-based routing with createFileRoute()
. Components rely on router hooks and navigation functionality that don’t work in isolation.
// src/routes/login.tsx
export const Route = createFileRoute("/login")({
component: RouteComponent,
loader: async ({ context: { session } }) => {
if (session && !session.isAnonymous) throw redirect({ href: "/" });
return { session };
},
});
function RouteComponent() {
const router = useRouter();
const { session } = Route.useLoaderData();
const { back } = Route.useSearch();
return (
<Layout>
<Link to="/signup">Sign up</Link>
{/* component content */}
</Layout>
);
}
The Solution
Mock the router components and hooks with simplified implementations:
// tests/unit/routes/login.test.tsx
vi.mock("@tanstack/react-router", () => ({
Link: ({ children, to }: { children: React.ReactNode; to: string }) => (
<a href={to}>{children}</a>
),
useRouter: () => ({
navigate: vi.fn(),
}),
createFileRoute: (_path: string) => (routeOptions: any) => ({
options: routeOptions,
useLoaderData: vi.fn(),
useSearch: vi.fn(),
}),
}));
Why This Works
Link
→<a>
: Converts complex router links to simple anchor tags that testing-library can find and interact withuseRouter
mock: Provides essential methods likenavigate()
that components expectcreateFileRoute
mock: Returns a structure that preserves the component while making hooks mockable
Mocking Route-Specific Hooks
File routes create hooks like Route.useLoaderData()
and Route.useSearch()
that provide server-loaded data and URL parameters.
The Solution
Create a test utility that controls these hooks:
// Test utility for rendering with controlled data
function renderLogin(session: any = null, searchParams: any = {}) {
// Mock the router hooks that the component uses
vi.mocked(LoginRoute.useLoaderData).mockReturnValue({ session });
vi.mocked(LoginRoute.useSearch).mockReturnValue(searchParams);
const user = userEvent.setup();
return { user, ...render(<LoginComponent />) };
}
// Usage in tests
it("handles custom redirect URLs", () => {
renderLogin(null, { back: "/custom-path" });
// Test that social login uses the custom callback URL
});
Why This Works
- Controlled data: Tests can provide specific session states and search parameters
- Isolated testing: No need for complex router setup or server-side loading
- Deterministic: Same inputs always produce same outputs
Mocking TanStack Query
Components using useQuery
depend on TanStack Query’s client and caching system.
// src/routes/certifications.tsx
function RouteComponent() {
const trpc = useTRPC();
const titlesQuery = useQuery(trpc.titles.listWithChapters.queryOptions());
return (
<Layout>
{titlesQuery.isLoading && <div>Loading...</div>}
{titlesQuery.data?.map(title => (
<Link key={title.id} to="/certification/$slug" params={{ slug: title.slug }}>
{title.name}
</Link>
))}
</Layout>
);
}
The Solution
Mock useQuery
to control loading states and data:
vi.mock("@tanstack/react-query", async () => {
const actual = await vi.importActual("@tanstack/react-query");
return {
...actual,
useQuery: vi.fn(),
};
});
// Test utility with controlled query states
function renderCertifications(mockQueryState: {
data?: TitleData[];
isLoading?: boolean;
isError?: boolean;
} = {}) {
vi.mocked(useQuery).mockReturnValue({
data: mockQueryState.data,
isLoading: mockQueryState.isLoading ?? false,
isError: mockQueryState.isError ?? false,
error: null,
isSuccess: !mockQueryState.isLoading && !mockQueryState.isError,
refetch: vi.fn(),
} as any);
return render(
<QueryClientProvider client={queryClient}>
<CertificationsComponent />
</QueryClientProvider>
);
}
// Usage in tests
it("shows loading state", () => {
renderCertifications({ isLoading: true });
expect(screen.getByText("Loading...")).toBeInTheDocument();
});
it("renders data when loaded", () => {
renderCertifications({ data: mockTitlesData });
expect(screen.getByText("React Fundamentals")).toBeInTheDocument();
});
Why This Works
- State control: Tests can simulate loading, success, and error states
- Data control: Provide specific data scenarios for comprehensive testing
- Performance: No actual network requests or caching complexity
Mocking tRPC Integration
TanStack Start often integrates with tRPC for type-safe API calls. Components expect tRPC query options and client methods.
The Solution
Mock the tRPC client structure:
vi.mock("/src/trpc/react", () => ({
useTRPC: () => ({
titles: {
listWithChapters: {
queryOptions: () => ({
queryKey: ["titles", "listWithChapters"],
queryFn: vi.fn(),
}),
},
},
}),
}));
Why This Works
- Interface compliance: Provides the expected tRPC client structure
- Query integration: Works seamlessly with TanStack Query mocking
- Type safety: Maintains TypeScript compatibility
Mocking External Dependencies
Components often depend on utility functions, UI components, and external libraries.
The Solution
Mock each dependency with focused implementations:
// Mock Layout component for testing
vi.mock("/src/components/Layout", () => ({
default: ({ children, title, icons }: {
children: React.ReactNode;
title: string;
icons?: string;
}) => (
<div data-testid="layout" data-title={title} data-icons={icons}>
{children}
</div>
),
}));
// Mock utility functions
vi.mock("/src/lib/clientUtils", () => ({
stripedBorderStyle: (color: string) => ({
border: `2px solid ${color}`,
backgroundColor: color,
}),
}));
// Mock authentication client
vi.mock("/src/lib/authClient", () => ({
authClient: {
signIn: {
social: vi.fn().mockResolvedValue({}),
emailOtp: vi.fn().mockResolvedValue({ success: true }),
anonymous: vi.fn().mockResolvedValue({}),
},
emailOtp: {
sendVerificationOtp: vi.fn().mockResolvedValue({ success: true }),
},
},
}));
Why This Works
- Simplified behavior: Focus on what’s important for the test
- Predictable responses: Avoid external API calls and side effects
- Test-friendly: Add test-specific attributes (like
data-testid
)
Complete Example: Testing a Login Component
Here’s how all these strategies work together:
import { render, screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
// Mock all dependencies
vi.mock("/src/components/Layout", () => ({
default: ({ children, title }) => (
<div data-testid="layout" data-title={title}>{children}</div>
),
}));
vi.mock("/src/lib/authClient", () => ({
authClient: {
signIn: {
social: vi.fn().mockResolvedValue({}),
emailOtp: vi.fn().mockResolvedValue({ success: true }),
},
},
}));
vi.mock("@tanstack/react-router", () => ({
Link: ({ children, to }) => <a href={to}>{children}</a>,
useRouter: () => ({ navigate: vi.fn() }),
createFileRoute: (_path: string) => (routeOptions: any) => ({
options: routeOptions,
useLoaderData: vi.fn(),
useSearch: vi.fn(),
}),
}));
import { authClient } from "/src/lib/authClient";
import { Route as LoginRoute } from "/src/routes/login";
const LoginComponent = LoginRoute.options.component;
function renderLogin(session: any = null, searchParams: any = {}) {
vi.mocked(LoginRoute.useLoaderData).mockReturnValue({ session });
vi.mocked(LoginRoute.useSearch).mockReturnValue(searchParams);
return { user: userEvent.setup(), ...render(<LoginComponent />) };
}
describe("Login Page", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders login form", async () => {
renderLogin();
await waitFor(() => {
expect(screen.getByText("Welcome Back")).toBeInTheDocument();
});
expect(screen.getByLabelText(/email address/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /continue with google/i })).toBeInTheDocument();
});
it("handles social login", async () => {
const { user } = renderLogin();
const googleButton = screen.getByRole("button", { name: /continue with google/i });
await user.click(googleButton);
expect(vi.mocked(authClient.signIn.social)).toHaveBeenCalledWith({
provider: "google",
callbackURL: "/todo",
});
});
it("uses custom callback URL", async () => {
const { user } = renderLogin(null, { back: "/custom-path" });
const googleButton = screen.getByRole("button", { name: /continue with google/i });
await user.click(googleButton);
expect(vi.mocked(authClient.signIn.social)).toHaveBeenCalledWith({
provider: "google",
callbackURL: "/custom-path",
});
});
});
Conclusion
This mocking approach for TanStack Start components prioritizes simplicity and practicality:
- Simplify complex dependencies while preserving essential behavior
- Control data flow with test utilities and mock configurations
- Focus on user interactions rather than internal implementation
- Handle async operations properly with appropriate waiting
While this approach may not follow all testing best practices (particularly for TanStack Query), it provides fast, reliable tests that catch real bugs and remain maintainable as your application evolves. The key is finding the right balance between realistic behavior and test simplicity.
For the AI-assisted learning platform at knowledgegap.guide, this testing strategy has enabled confident development and refactoring while maintaining good test coverage across complex component interactions.
Feedback Welcome
Since TanStack Start is relatively new and testing patterns are still evolving, I’d love to hear about alternative approaches or improvements to these strategies. Please reach out:
- Email: Contact me directly
- X/Twitter: @thasmin
I’ll update this post with better approaches as the ecosystem and best practices continue to mature.