| name | nextjs-tanstack-stack |
| description | Use when building Next.js applications with TanStack ecosystem (Table, Query, Form, Virtual), Zustand state management, or implementing data-intensive dashboards, virtualized tables, forms with validation, or performance optimization. Triggers on "Next.js App Router", "TanStack", "data table", "virtualization", "memoization", "server components", "client state", "Zustand". |
Next.js + TanStack Stack Implementation
Production patterns for Next.js App Router applications using TanStack ecosystem and Zustand, aligned with boring-over-clever philosophy.
Stack Coverage
| Library | Version | Purpose |
|---|
| Next.js | 14+ | App Router, RSC |
| TanStack Query | v5 | Server state |
| TanStack Table | v8 | Data grids |
| TanStack Form | v1 | Form handling |
| TanStack Virtual | v3 | List virtualization |
| Zustand | v4+ | Client state |
When to Use This Skill
Use for:
- Data-intensive dashboards and tables
- Forms with complex validation and field arrays
- Large list rendering with virtualization
- Server/client component architecture
- Integrating server and client state
Delegate to other skills:
- Visual design decisions →
frontend-design
- UX planning and wireframes →
ux-designer
- Browser automation testing →
webapp-testing
Core Architecture Patterns
1. Server vs Client Component Boundary
Server Component (default):
├─ Data fetching at edge
├─ No interactivity needed
├─ SEO-critical content
└─ Secret/env access
Client Component ('use client'):
├─ Event handlers (onClick, onChange)
├─ Browser APIs (localStorage, window)
├─ useState, useEffect, useRef
├─ TanStack hooks (Query/Form/Table)
└─ Zustand stores
Composition Pattern:
import { MarketTable } from './market-table';
import { getMarkets } from '@/lib/api';
export default async function MarketsPage() {
const initialData = await getMarkets();
return <MarketTable initialData={initialData} />;
}
'use client';
import { useQuery } from '@tanstack/react-query';
import { getMarkets } from '@/lib/api';
interface Props {
initialData: Awaited<ReturnType<typeof getMarkets>>;
}
export function MarketTable({ initialData }: Props) {
const { data } = useQuery({
queryKey: ['markets'],
queryFn: getMarkets,
initialData,
});
return <Table = />;
}
2. Provider Architecture
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState, type ReactNode } from 'react';
export function Providers({ children }: { children: ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 1000,
retry: (failureCount, error) => {
if ((error as { status?: number }).status === 404) return false;
return failureCount < 3;
},
},
mutations: {
onError: (error) => {
console.error('[Mutation Error]', { error, timestamp: new Date().toISOString() });
},
},
},
}));
(
);
}
3. TanStack Query Patterns
Query Key Factory:
export const queryKeys = {
markets: {
all: ['markets'] as const,
detail: (id: string) => ['markets', id] as const,
filtered: (filters: MarketFilters) => ['markets', 'filtered', filters] as const,
},
trades: {
byMarket: (marketId: string) => ['trades', marketId] as const,
},
} as const;
Query Hook with Select:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query-keys';
export function useMarkets(filters?: MarketFilters) {
return useQuery({
queryKey: filters ? queryKeys.markets.filtered(filters) : queryKeys.markets.all,
queryFn: () => fetchMarkets(filters),
select: (data) => data.filter((m) => m.active),
});
}
export function useUpdateMarket() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateMarket,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.markets.all });
},
onError: (error, variables) => {
console.(, { error, : variables. });
},
});
}
4. TanStack Table + Virtualization
'use client';
import { useVirtualizer } from '@tanstack/react-virtual';
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
type ColumnDef,
type SortingState,
} from '@tanstack/react-table';
import { useRef, useState, useMemo } from 'react';
interface VirtualizedTableProps<T> {
data: T[];
columns: ColumnDef<T>[];
estimateRowHeight?: number;
}
export function VirtualizedTable<T>({
data,
columns,
estimateRowHeight = 48,
}: VirtualizedTableProps<T>) {
const parentRef = useRef<HTMLDivElement>(null);
const [sorting, setSorting] = useState<SortingState>([]);
const table = useReactTable({
data,
columns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
const { rows } = table.getRowModel();
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: parentRef.,
: estimateRowHeight,
: ,
});
virtualRows = virtualizer.();
(
);
}
5. Zustand Store Patterns
Slice Pattern with Actions Namespace:
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface MarketFiltersState {
search: string;
category: string | null;
sortBy: 'volume' | 'price' | 'name';
actions: {
setSearch: (search: string) => void;
setCategory: (category: string | null) => void;
setSortBy: (sortBy: 'volume' | 'price' | 'name') => void;
reset: () => void;
};
}
const initialState = {
search: '',
category: null,
sortBy: 'volume' as const,
};
export const useMarketFilters = create<MarketFiltersState>()(
(
(
({
...initialState,
: {
: ({ search }),
: ({ category }),
: ({ sortBy }),
: (initialState),
},
}),
{ : }
),
{ : }
)
);
= () => state.;
= () => state.;
= () => state.;
= () => state.;
Usage with Atomic Selectors:
function SearchInput() {
const search = useMarketFilters(selectSearch);
const { setSearch } = useMarketFilters(selectActions);
return <input value={search} onChange={(e) => setSearch(e.target.value)} />;
}
6. Error Boundary Pattern
'use client';
import { Component, type ReactNode, type ErrorInfo } from 'react';
interface Props {
children: ReactNode;
fallback: (error: Error, reset: () => void) => ReactNode;
onError?: (error: Error, info: ErrorInfo) => void;
}
interface State {
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { error: null };
static getDerivedStateFromError(error: Error): State {
return { error };
}
componentDidCatch(: , : ) {
.(, {
: error.,
: error.,
: info.,
});
..?.(error, info);
}
reset = {
.({ : });
};
() {
(..) {
..(.., .);
}
..;
}
}
With TanStack Query:
import { QueryErrorResetBoundary } from '@tanstack/react-query';
function MarketDashboard() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
fallback={(error, localReset) => (
<div>
<p>Error: {error.message}</p>
<button onClick={() => { reset(); localReset(); }}>
Retry
</button>
</div>
)}
>
<MarketTable />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}
7. TanStack Form Pattern
'use client';
import { useForm } from '@tanstack/react-form';
import { zodValidator } from '@tanstack/zod-form-adapter';
import { z } from 'zod';
import { useCreateMarket } from '@/hooks/use-markets';
const marketSchema = z.object({
name: z.string().min(1, 'Required').max(100),
description: z.string().min(10, 'At least 10 characters'),
outcomes: z.array(z.object({
label: z.string().min(1),
probability: z.number().min(0).max(100),
})).min(2, 'At least 2 outcomes'),
});
type MarketFormData = z.infer<typeof marketSchema>;
export function CreateMarketForm() {
const createMutation = ();
form = ({
: {
: ,
: ,
: [{ : , : }, { : , : }],
} ,
: (),
: {
: marketSchema,
},
: ({ value }) => {
createMutation.(value);
},
});
(
);
}
Performance Optimization Checklist
- Memoization:
useMemo for derived data, useCallback for handlers passed to children
- Selector Pattern: Extract atomic selectors from Zustand stores
- Query Deduplication: Consistent query keys prevent duplicate requests
- Virtualization: TanStack Virtual for lists > 100 items
- Code Splitting:
dynamic() for route-level, lazy() for component-level
- Suspense Boundaries: Wrap data-fetching at feature boundaries
Best Practices
- Keep Server Components for static content and initial data fetching
- Colocate Query hooks with components that use them
- Use factory functions for query keys (
queryKeys.markets.detail(id))
- Separate Zustand actions into
actions namespace
- Apply error boundaries at feature boundaries, not globally
- Prefer
select in useQuery over deriving in render
- Use TypeScript strict mode with explicit return types
- Test hooks with React Testing Library + MSW
Common Pitfalls
- 'use client' everywhere: Breaks RSC benefits; only add when needed
- New QueryClient on render: Use
useState(() => new QueryClient())
- Full store subscription: Use atomic selectors, not
useStore((s) => s)
- Mixed state boundaries: Keep server/client state clearly separated
- Silent mutation errors: Always log with context in
onError
- Over-memoizing: Don't memoize primitives or static arrays
Resources
Reference Files
references/tanstack-query-patterns.md — Query factories, caching, optimistic updates
references/tanstack-table-patterns.md — Column defs, sorting, filtering, virtualization
references/tanstack-form-patterns.md — Validation, field arrays, async submission
references/zustand-patterns.md — Store slicing, middleware, persistence
references/nextjs-app-router.md — Layouts, streaming, parallel routes
references/performance-patterns.md — Profiling, bundle analysis, optimization
Example Files