Back to blog

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.

Let's go! 🦅

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments