TanStack in 2026: The Unified Ecosystem Dominating React
Hello HaWkers, if you work with React in 2026, you've probably heard of TanStack. What started as React Query has transformed into a complete ecosystem that's changing how we build applications.
Let's explore each part of this ecosystem and how to use it in practice.
The TanStack Ecosystem
Overview in 2026
// What TanStack offers today
const tanstackEcosystem = {
query: {
purpose: 'Server state management',
status: 'Mature and stable',
adoption: 'Market standard'
},
router: {
purpose: 'Type-safe routing',
status: 'Serious alternative to React Router',
feature: 'File-based + type safety'
},
table: {
purpose: 'Complex tables and grids',
status: 'Category leader',
feature: 'Headless, extensible'
},
form: {
purpose: 'Form management',
status: 'Growing rapidly',
feature: 'Type-safe, performant'
},
store: {
purpose: 'Client state management',
status: 'Lightweight alternative to Redux/Zustand',
feature: 'Signals-based'
},
db: {
purpose: 'Local-first database',
status: 'New in 2025-2026',
feature: 'Sync, offline, real-time'
},
start: {
purpose: 'Full-stack framework',
status: 'Advanced beta',
feature: 'SSR, file routing, API routes'
},
ai: {
purpose: 'AI/LLM integrations',
status: 'Experimental',
feature: 'Streaming, hooks for AI'
}
};
TanStack Query
The Heart of the Ecosystem
// TanStack Query - Server State Management
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
// Setup
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: 3,
refetchOnWindowFocus: true,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
</QueryClientProvider>
);
}
// Basic usage
interface User {
id: number;
name: string;
email: string;
}
function useUser(userId: number) {
return useQuery({
queryKey: ['user', userId],
queryFn: async (): Promise<User> => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
},
});
}
function UserProfile({ userId }: { userId: number }) {
const { data: user, isLoading, error } = useUser(userId);
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}Mutations and Optimistic Updates
// Mutations with optimistic updates
interface UpdateUserData {
name?: string;
email?: string;
}
function useUpdateUser(userId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: UpdateUserData) => {
const res = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error('Failed to update');
return res.json();
},
// Optimistic update
onMutate: async (newData) => {
// Cancel ongoing queries
await queryClient.cancelQueries({ queryKey: ['user', userId] });
// Snapshot previous value
const previousUser = queryClient.getQueryData<User>(['user', userId]);
// Optimistically update
queryClient.setQueryData<User>(['user', userId], (old) => ({
...old!,
...newData,
}));
// Return context for rollback
return { previousUser };
},
// If error, rollback
onError: (err, newData, context) => {
if (context?.previousUser) {
queryClient.setQueryData(['user', userId], context.previousUser);
}
},
// Always refetch after mutation
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['user', userId] });
},
});
}
// Usage
function EditUserForm({ userId }: { userId: number }) {
const { data: user } = useUser(userId);
const updateUser = useUpdateUser(userId);
const [name, setName] = useState(user?.name ?? '');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
updateUser.mutate({ name });
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button disabled={updateUser.isPending}>
{updateUser.isPending ? 'Saving...' : 'Save'}
</button>
</form>
);
}
TanStack Router
Type-Safe Routing
// TanStack Router - Routing with total type safety
import {
createRootRoute,
createRoute,
createRouter,
RouterProvider,
Link,
Outlet,
} from '@tanstack/react-router';
// Define the root route
const rootRoute = createRootRoute({
component: () => (
<div>
<nav>
<Link to="/">Home</Link>
<Link to="/users">Users</Link>
<Link to="/users/$userId" params={{ userId: '1' }}>
User 1
</Link>
</nav>
<Outlet />
</div>
),
});
// Home route
const homeRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <h1>Home</h1>,
});
// Users list route
const usersRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users',
component: UsersPage,
// Loader for data prefetch
loader: async () => {
const users = await fetchUsers();
return { users };
},
});
// User detail route with typed parameter
const userRoute = createRoute({
getParentRoute: () => usersRoute,
path: '$userId',
component: UserDetail,
// Parameter validation
parseParams: (params) => ({
userId: parseInt(params.userId),
}),
stringifyParams: (params) => ({
userId: String(params.userId),
}),
// Loader with access to typed params
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
return { user };
},
});
// Route tree
const routeTree = rootRoute.addChildren([
homeRoute,
usersRoute.addChildren([userRoute]),
]);
// Router instance
const router = createRouter({ routeTree });
// Type registration for global type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
// App
function App() {
return <RouterProvider router={router} />;
}
// Components using loader data
function UsersPage() {
const { users } = usersRoute.useLoaderData();
return (
<ul>
{users.map((user) => (
<li key={user.id}>
<Link to="/users/$userId" params={{ userId: user.id }}>
{user.name}
</Link>
</li>
))}
</ul>
);
}
function UserDetail() {
const { user } = userRoute.useLoaderData();
const { userId } = userRoute.useParams();
return (
<div>
<h2>{user.name}</h2>
<p>ID: {userId}</p>
</div>
);
}
TanStack Form
Type-Safe Forms
// TanStack Form - Form management
import { useForm } from '@tanstack/react-form';
import { z } from 'zod';
// Validation schema
const userSchema = z.object({
name: z.string().min(2, 'Name must have at least 2 characters'),
email: z.string().email('Invalid email'),
age: z.number().min(18, 'Must be over 18'),
role: z.enum(['admin', 'user', 'guest']),
});
type UserFormData = z.infer<typeof userSchema>;
function UserForm() {
const form = useForm<UserFormData>({
defaultValues: {
name: '',
email: '',
age: 18,
role: 'user',
},
onSubmit: async ({ value }) => {
// value is fully typed
await saveUser(value);
},
validators: {
onChange: userSchema,
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.Field
name="name"
children={(field) => (
<div>
<label>Name</label>
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.length > 0 && (
<span className="error">
{field.state.meta.errors.join(', ')}
</span>
)}
</div>
)}
/>
<form.Field
name="email"
children={(field) => (
<div>
<label>Email</label>
<input
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.length > 0 && (
<span className="error">
{field.state.meta.errors.join(', ')}
</span>
)}
</div>
)}
/>
<form.Field
name="age"
children={(field) => (
<div>
<label>Age</label>
<input
type="number"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(parseInt(e.target.value))}
/>
</div>
)}
/>
<form.Field
name="role"
children={(field) => (
<div>
<label>Role</label>
<select
value={field.state.value}
onChange={(e) =>
field.handleChange(e.target.value as UserFormData['role'])
}
>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="guest">Guest</option>
</select>
</div>
)}
/>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save'}
</button>
)}
/>
</form>
);
}
TanStack Table
Complex Tables
// TanStack Table - Headless tables
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
createColumnHelper,
SortingState,
} from '@tanstack/react-table';
interface User {
id: number;
name: string;
email: string;
role: string;
createdAt: Date;
}
const columnHelper = createColumnHelper<User>();
const columns = [
columnHelper.accessor('id', {
header: 'ID',
cell: (info) => info.getValue(),
}),
columnHelper.accessor('name', {
header: 'Name',
cell: (info) => info.getValue(),
}),
columnHelper.accessor('email', {
header: 'Email',
cell: (info) => (
<a href={`mailto:${info.getValue()}`}>{info.getValue()}</a>
),
}),
columnHelper.accessor('role', {
header: 'Role',
cell: (info) => <Badge>{info.getValue()}</Badge>,
}),
columnHelper.accessor('createdAt', {
header: 'Created At',
cell: (info) => info.getValue().toLocaleDateString(),
sortingFn: 'datetime',
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: (info) => (
<div>
<button onClick={() => editUser(info.row.original)}>Edit</button>
<button onClick={() => deleteUser(info.row.original.id)}>
Delete
</button>
</div>
),
}),
];
function UsersTable({ data }: { data: User[] }) {
const [sorting, setSorting] = useState<SortingState>([]);
const [globalFilter, setGlobalFilter] = useState('');
const table = useReactTable({
data,
columns,
state: {
sorting,
globalFilter,
},
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<div>
{/* Global filter */}
<input
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="Search..."
/>
{/* Table */}
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
onClick={header.column.getToggleSortingHandler()}
style={{ cursor: 'pointer' }}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getIsSorted() === 'asc' && ' 🔼'}
{header.column.getIsSorted() === 'desc' && ' 🔽'}
</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>
{/* Pagination */}
<div>
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</button>
<span>
Page {table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</span>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</button>
</div>
</div>
);
}
TanStack Store
Lightweight State Management
// TanStack Store - Client state with signals
import { Store, useStore } from '@tanstack/store';
// Create store
interface AppState {
user: {
id: number;
name: string;
} | null;
theme: 'light' | 'dark';
notifications: string[];
}
const store = new Store<AppState>({
user: null,
theme: 'light',
notifications: [],
});
// Actions as functions
function login(user: AppState['user']) {
store.setState((state) => ({
...state,
user,
}));
}
function logout() {
store.setState((state) => ({
...state,
user: null,
}));
}
function toggleTheme() {
store.setState((state) => ({
...state,
theme: state.theme === 'light' ? 'dark' : 'light',
}));
}
function addNotification(message: string) {
store.setState((state) => ({
...state,
notifications: [...state.notifications, message],
}));
}
// Usage in components
function UserMenu() {
// Select only what you need
const user = useStore(store, (state) => state.user);
if (!user) {
return <button onClick={() => login({ id: 1, name: 'John' })}>Login</button>;
}
return (
<div>
<span>Hello, {user.name}</span>
<button onClick={logout}>Logout</button>
</div>
);
}
function ThemeToggle() {
const theme = useStore(store, (state) => state.theme);
return (
<button onClick={toggleTheme}>
Theme: {theme === 'light' ? '☀️' : '🌙'}
</button>
);
}
function NotificationBell() {
const count = useStore(store, (state) => state.notifications.length);
return (
<button>
🔔 {count > 0 && <span className="badge">{count}</span>}
</button>
);
}
Integrating the Ecosystem
Complete Example
// Complete app using multiple TanStack tools
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RouterProvider, createRouter } from '@tanstack/react-router';
import { Store } from '@tanstack/store';
// Global setup
const queryClient = new QueryClient();
const appStore = new Store({ theme: 'light' as const });
// Router with Query integration
const rootRoute = createRootRoute({
component: () => {
const theme = useStore(appStore, (s) => s.theme);
return (
<div className={theme}>
<Header />
<Outlet />
</div>
);
},
});
const usersRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users',
// Prefetch with Query
loader: async ({ context }) => {
await context.queryClient.ensureQueryData({
queryKey: ['users'],
queryFn: fetchUsers,
});
},
component: () => {
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return <UsersTable data={users ?? []} />;
},
});
// Router with context
const router = createRouter({
routeTree: rootRoute.addChildren([usersRoute]),
context: {
queryClient,
},
});
// App
function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}Conclusion
TanStack in 2026 is no longer "just React Query" - it's a complete and cohesive ecosystem for building modern React applications. The main advantage is consistency: similar APIs, type safety everywhere, and seamless integration between parts.
When to use each part:
- Query: Whenever you have server data
- Router: If you want type safety in routing (and want to move away from React Router)
- Table: Complex tables with sort/filter/pagination
- Form: Medium to complex forms
- Store: Simple global state (alternative to Zustand)
The ecosystem continues to grow, and TanStack Start promises to be an interesting alternative to Next.js for those who want maximum control with excellent DX.
To learn more about the current state of React, check out: React Compiler in 2026.

