Voltar para o Blog

TanStack em 2026: O Ecossistema Unificado Que Está Dominando o React

Olá HaWkers, se você trabalha com React em 2026, provavelmente já ouviu falar do TanStack. O que começou como React Query se transformou em um ecossistema completo que está mudando a forma como construímos aplicações.

Vamos explorar cada parte desse ecossistema e como usar na prática.

O Ecossistema TanStack

Visão Geral em 2026

// O que o TanStack oferece hoje

const tanstackEcosystem = {
  query: {
    purpose: 'Server state management',
    status: 'Maduro e estável',
    adoption: 'Padrão de mercado'
  },

  router: {
    purpose: 'Type-safe routing',
    status: 'Alternativa séria ao React Router',
    feature: 'File-based + type safety'
  },

  table: {
    purpose: 'Tabelas e grids complexos',
    status: 'Líder na categoria',
    feature: 'Headless, extensível'
  },

  form: {
    purpose: 'Gerenciamento de formulários',
    status: 'Crescendo rapidamente',
    feature: 'Type-safe, performático'
  },

  store: {
    purpose: 'Client state management',
    status: 'Alternativa leve ao Redux/Zustand',
    feature: 'Signals-based'
  },

  db: {
    purpose: 'Local-first database',
    status: 'Novo em 2025-2026',
    feature: 'Sync, offline, real-time'
  },

  start: {
    purpose: 'Full-stack framework',
    status: 'Beta avançado',
    feature: 'SSR, file routing, API routes'
  },

  ai: {
    purpose: 'AI/LLM integrations',
    status: 'Experimental',
    feature: 'Streaming, hooks para AI'
  }
};

TanStack Query

O Coração do Ecossistema

// 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 e Optimistic Updates

// Mutations com 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 em andamento
      await queryClient.cancelQueries({ queryKey: ['user', userId] });

      // Snapshot do valor anterior
      const previousUser = queryClient.getQueryData<User>(['user', userId]);

      // Otimisticamente atualiza
      queryClient.setQueryData<User>(['user', userId], (old) => ({
        ...old!,
        ...newData,
      }));

      // Retorna contexto para rollback
      return { previousUser };
    },

    // Se erro, rollback
    onError: (err, newData, context) => {
      if (context?.previousUser) {
        queryClient.setQueryData(['user', userId], context.previousUser);
      }
    },

    // Sempre refetch após 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 ? 'Salvando...' : 'Salvar'}
      </button>
    </form>
  );
}

TanStack Router

Type-Safe Routing

// TanStack Router - Routing com type safety total

import {
  createRootRoute,
  createRoute,
  createRouter,
  RouterProvider,
  Link,
  Outlet,
} from '@tanstack/react-router';

// Define a 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 dados
  loader: async () => {
    const users = await fetchUsers();
    return { users };
  },
});

// User detail route com parâmetro tipado
const userRoute = createRoute({
  getParentRoute: () => usersRoute,
  path: '$userId',
  component: UserDetail,
  // Validação de parâmetros
  parseParams: (params) => ({
    userId: parseInt(params.userId),
  }),
  stringifyParams: (params) => ({
    userId: String(params.userId),
  }),
  // Loader com acesso aos 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 dados do 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

Formulários Type-Safe

// TanStack Form - Gerenciamento de formulários

import { useForm } from '@tanstack/react-form';
import { z } from 'zod';

// Schema de validação
const userSchema = z.object({
  name: z.string().min(2, 'Nome deve ter pelo menos 2 caracteres'),
  email: z.string().email('Email inválido'),
  age: z.number().min(18, 'Deve ser maior 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 é totalmente tipado
      await saveUser(value);
    },
    validators: {
      onChange: userSchema,
    },
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        form.handleSubmit();
      }}
    >
      <form.Field
        name="name"
        children={(field) => (
          <div>
            <label>Nome</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>Idade</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 ? 'Salvando...' : 'Salvar'}
          </button>
        )}
      />
    </form>
  );
}

TanStack Table

Tabelas Complexas

// TanStack Table - Tabelas 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: 'Nome',
    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: 'Criado em',
    cell: (info) => info.getValue().toLocaleDateString(),
    sortingFn: 'datetime',
  }),
  columnHelper.display({
    id: 'actions',
    header: 'Ações',
    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>
      {/* Filtro global */}
      <input
        value={globalFilter}
        onChange={(e) => setGlobalFilter(e.target.value)}
        placeholder="Buscar..."
      />

      {/* Tabela */}
      <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>

      {/* Paginação */}
      <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()}
        >
          Próxima
        </button>
      </div>
    </div>
  );
}

TanStack Store

State Management Leve

// TanStack Store - Client state com signals

import { Store, useStore } from '@tanstack/store';

// Criar 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 funções
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 em componentes
function UserMenu() {
  // Seleciona apenas o que precisa
  const user = useStore(store, (state) => state.user);

  if (!user) {
    return <button onClick={() => login({ id: 1, name: 'John' })}>Login</button>;
  }

  return (
    <div>
      <span>Olá, {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 o Ecossistema

Exemplo Completo

// App completa usando múltiplos 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 com integração 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 com 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 com context
const router = createRouter({
  routeTree: rootRoute.addChildren([usersRoute]),
  context: {
    queryClient,
  },
});

// App
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <RouterProvider router={router} />
    </QueryClientProvider>
  );
}

Conclusão

O TanStack em 2026 não é mais "só o React Query" - é um ecossistema completo e coeso para construir aplicações React modernas. A vantagem principal é a consistência: APIs similares, type safety em tudo, e integração perfeita entre as partes.

Quando usar cada parte:

  • Query: Sempre que tiver dados do servidor
  • Router: Se quer type safety no routing (e quer sair do React Router)
  • Table: Tabelas complexas com sort/filter/pagination
  • Form: Formulários médios a complexos
  • Store: Estado global simples (alternativa ao Zustand)

O ecossistema continua crescendo, e TanStack Start promete ser uma alternativa interessante ao Next.js para quem quer máximo controle com DX excelente.

Para entender mais sobre o estado atual do React, confira: React Compiler em 2026.

Bora pra cima! 🦅

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário