TanStack en 2026: El Ecosistema Unificado Que Está Dominando React
Hola HaWkers, si trabajas con React en 2026, probablemente ya escuchaste hablar de TanStack. Lo que comenzó como React Query se transformó en un ecosistema completo que está cambiando la forma como construimos aplicaciones.
Vamos a explorar cada parte de este ecosistema y cómo usarlo en la práctica.
El Ecosistema TanStack
Visión General en 2026
// Lo que TanStack ofrece hoy
const tanstackEcosystem = {
query: {
purpose: 'Server state management',
status: 'Maduro y estable',
adoption: 'Estándar del mercado'
},
router: {
purpose: 'Type-safe routing',
status: 'Alternativa seria a React Router',
feature: 'File-based + type safety'
},
table: {
purpose: 'Tablas y grids complejos',
status: 'Líder en la categoría',
feature: 'Headless, extensible'
},
form: {
purpose: 'Gestión de formularios',
status: 'Creciendo rápidamente',
feature: 'Type-safe, performante'
},
store: {
purpose: 'Client state management',
status: 'Alternativa liviana a Redux/Zustand',
feature: 'Signals-based'
},
db: {
purpose: 'Local-first database',
status: 'Nuevo en 2025-2026',
feature: 'Sync, offline, real-time'
},
start: {
purpose: 'Full-stack framework',
status: 'Beta avanzado',
feature: 'SSR, file routing, API routes'
},
ai: {
purpose: 'AI/LLM integrations',
status: 'Experimental',
feature: 'Streaming, hooks para AI'
}
};
TanStack Query
El Corazón del Ecosistema
// 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 minutos
gcTime: 1000 * 60 * 30, // 30 minutos (antes era cacheTime)
retry: 3,
refetchOnWindowFocus: true,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
</QueryClientProvider>
);
}
// Uso básico
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 y Optimistic Updates
// Mutations con 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) => {
// Cancela queries en curso
await queryClient.cancelQueries({ queryKey: ['user', userId] });
// Snapshot del valor anterior
const previousUser = queryClient.getQueryData<User>(['user', userId]);
// Actualiza optimísticamente
queryClient.setQueryData<User>(['user', userId], (old) => ({
...old!,
...newData,
}));
// Retorna contexto para rollback
return { previousUser };
},
// Si hay error, rollback
onError: (err, newData, context) => {
if (context?.previousUser) {
queryClient.setQueryData(['user', userId], context.previousUser);
}
},
// Siempre refetch después de mutation
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['user', userId] });
},
});
}
// Uso
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 ? 'Guardando...' : 'Guardar'}
</button>
</form>
);
}
TanStack Router
Type-Safe Routing
// TanStack Router - Routing con type safety total
import {
createRootRoute,
createRoute,
createRouter,
RouterProvider,
Link,
Outlet,
} from '@tanstack/react-router';
// Define la 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 para prefetch de datos
loader: async () => {
const users = await fetchUsers();
return { users };
},
});
// User detail route con parámetro tipado
const userRoute = createRoute({
getParentRoute: () => usersRoute,
path: '$userId',
component: UserDetail,
// Validación de parámetros
parseParams: (params) => ({
userId: parseInt(params.userId),
}),
stringifyParams: (params) => ({
userId: String(params.userId),
}),
// Loader con acceso a params tipados
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 para type safety global
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
// App
function App() {
return <RouterProvider router={router} />;
}
// Componentes usando datos del loader
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
Formularios Type-Safe
// TanStack Form - Gestión de formularios
import { useForm } from '@tanstack/react-form';
import { z } from 'zod';
// Schema de validación
const userSchema = z.object({
name: z.string().min(2, 'Nombre debe tener al menos 2 caracteres'),
email: z.string().email('Email inválido'),
age: z.number().min(18, 'Debe ser mayor de 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 está totalmente tipado
await saveUser(value);
},
validators: {
onChange: userSchema,
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.Field
name="name"
children={(field) => (
<div>
<label>Nombre</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>Edad</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>Rol</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 ? 'Guardando...' : 'Guardar'}
</button>
)}
/>
</form>
);
}
TanStack Table
Tablas Complejas
// TanStack Table - Tablas headless
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: 'Nombre',
cell: (info) => info.getValue(),
}),
columnHelper.accessor('email', {
header: 'Email',
cell: (info) => (
<a href={`mailto:${info.getValue()}`}>{info.getValue()}</a>
),
}),
columnHelper.accessor('role', {
header: 'Rol',
cell: (info) => <Badge>{info.getValue()}</Badge>,
}),
columnHelper.accessor('createdAt', {
header: 'Creado en',
cell: (info) => info.getValue().toLocaleDateString(),
sortingFn: 'datetime',
}),
columnHelper.display({
id: 'actions',
header: 'Acciones',
cell: (info) => (
<div>
<button onClick={() => editUser(info.row.original)}>Editar</button>
<button onClick={() => deleteUser(info.row.original.id)}>
Eliminar
</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>
{/* Filtro global */}
<input
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="Buscar..."
/>
{/* Tabla */}
<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>
{/* Paginación */}
<div>
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Anterior
</button>
<span>
Página {table.getState().pagination.pageIndex + 1} de{' '}
{table.getPageCount()}
</span>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Siguiente
</button>
</div>
</div>
);
}
TanStack Store
State Management Ligero
// TanStack Store - Client state con signals
import { Store, useStore } from '@tanstack/store';
// Crear 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 como funciones
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],
}));
}
// Uso en componentes
function UserMenu() {
// Selecciona solo lo que necesita
const user = useStore(store, (state) => state.user);
if (!user) {
return <button onClick={() => login({ id: 1, name: 'John' })}>Login</button>;
}
return (
<div>
<span>Hola, {user.name}</span>
<button onClick={logout}>Logout</button>
</div>
);
}
function ThemeToggle() {
const theme = useStore(store, (state) => state.theme);
return (
<button onClick={toggleTheme}>
Tema: {theme === 'light' ? '☀️' : '🌙'}
</button>
);
}
function NotificationBell() {
const count = useStore(store, (state) => state.notifications.length);
return (
<button>
🔔 {count > 0 && <span className="badge">{count}</span>}
</button>
);
}
Integrando el Ecosistema
Ejemplo Completo
// App completa usando múltiples TanStack
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RouterProvider, createRouter } from '@tanstack/react-router';
import { Store } from '@tanstack/store';
// Setup global
const queryClient = new QueryClient();
const appStore = new Store({ theme: 'light' as const });
// Router con integración Query
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 con 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 con contexto
const router = createRouter({
routeTree: rootRoute.addChildren([usersRoute]),
context: {
queryClient,
},
});
// App
function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}Conclusión
TanStack en 2026 ya no es "solo React Query" - es un ecosistema completo y cohesivo para construir aplicaciones React modernas. La ventaja principal es la consistencia: APIs similares, type safety en todo, e integración perfecta entre las partes.
Cuándo usar cada parte:
- Query: Siempre que tengas datos del servidor
- Router: Si quieres type safety en el routing (y quieres salir de React Router)
- Table: Tablas complejas con sort/filter/pagination
- Form: Formularios medios a complejos
- Store: Estado global simple (alternativa a Zustand)
El ecosistema continúa creciendo, y TanStack Start promete ser una alternativa interesante a Next.js para quien quiere máximo control con DX excelente.
Para entender más sobre el estado actual de React, consulta: React Compiler en 2026.

