Ir para o conteúdo
decodecodeveloper docs
Storefront → Templates → Commerce

VTEX — hooks

Cart v2 (createCart, useAddToCart, useCartSummary), além de useCart, useUser, useWishlist, useAutocomplete para o browser.

@decocms/apps-vtex/hooks provê hooks React para as interações de UI commerce mais comuns. Use-os em client components em vez de chamar fetch direto.

Duas gerações de carrinho. O Cart v2 (a factory createCart + hooks granulares) é o caminho recomendado para código novo — é econômico em chamadas de API, lazy e agnóstico de framework. O useCart original mais abaixo ("Carrinho legado") continua funcionando e intocado, mas cria um OrderForm da VTEX para todo visitante e sempre devolve o payload completo (~40 KB). useUser, useWishlist e useAutocomplete não são afetados por essa separação.

Cart v2 (recomendado)

O Cart v2 torna o tráfego do carrinho granular e lazy. Dois botões independentes por operação:

  • sections — o que você pede para a VTEX computar (expectedOrderFormSections). Menos sections = payload menor da VTEX.
  • projection — o que o servidor de fato envia ao browser, independente de sections.

Todo hook usa por padrão a opção mais barata; pedir mais é sempre explícito. Nenhum OrderForm é criado até o primeiro add-to-cart — um visitante que só navega gera zero chamadas a /api/checkout/pub/orderForm.

Setup — createCart

Crie os hooks uma vez por site e injete o proxy invoke gerado:

// src/hooks/cart.ts
import { createCart } from "@decocms/apps-vtex/hooks/createCart";
import { invoke } from "~/server/invoke"; // seu invoke TanStack / Next.js gerado
 
export const {
  useCart, useCartSummary, useAddToCart, useShipping, useGifts, useAttachments, resetCart,
} = createCart({ invoke });

Cada chamada a createCart devolve um module-singleton independente — chame uma vez por site, em escopo de módulo. Hooks da mesma chamada compartilham estado; de chamadas diferentes ficam isolados.

Projections

projectionO que chega ao browserQuando usar
"none"{ ok: true }Update puramente otimista, sem reconciliação
"summary"{ orderFormId, totalItems, total }Só atualizar o badge
"summary+items"summary + itens enxutosPadrão do add-to-cart (toast)
"minicart"Minicart canônico completoAbrir o drawer
"raw"OrderForm cru da VTEXGTM / pixels / integrações custom

Regra de bolso: badge → summary, toast → summary+items, drawer → minicart. Os presets de sections (SECTIONS_MINIMAL, SECTIONS_DRAWER, SECTIONS_FULL) e os tipos do contrato ficam em @decocms/apps-commerce/types/cart.

useCartSummary() — badge

Lê estado local; nunca dispara chamada à VTEX sozinho.

function CartBadge() {
  const { totalItems, loading } = useCartSummary();
  return <span>{loading ? "…" : totalItems}</span>;
}

useAddToCart(opts?) — adicionar com update otimista embutido

function BuyButton({ id, seller }: { id: string; seller: string }) {
  const { add, loading } = useAddToCart(); // projection padrão: "summary+items"
  return (
    <button disabled={loading} onClick={() => add({ id, seller, quantity: 1 })}>
      Adicionar ao carrinho
    </button>
  );
}

No add(...): o badge incrementa otimisticamente → getOrCreateCartV2 roda só se ainda não existe carrinho (lazy) → addItemsToCartV2 é chamado com SECTIONS_MINIMAL + sua projection → a resposta projetada reconcilia o badge (ou o minicart completo) → em erro, o incremento otimista faz rollback. O add() retorna o payload projetado, então você dispara um toast sem um segundo fetch:

const res = await add({ id, seller }); // { totalItems, total, items: [slim] }
if (res.items?.[0]) toast(`Adicionado: ${res.items[0].item_name}`);

Passe projection: "minicart" para abrir o drawer direto da resposta, ou projection: "none" para uma UI puramente otimista.

useCart(opts?) — drawer / carrinho completo

const { minicart, summary, loading, updateQuantity, removeItem, addCoupon } = useCart({
  include: { full: open },   // só busca o carrinho completo quando o drawer abre
  freeShippingTarget: 15000,
  locale: "pt-BR",
  checkoutHref: "/checkout",
  enableCoupon: true,
});

include.full: false (padrão) expõe apenas o summary sem chamada à VTEX. updateQuantity, removeItem e addCoupon chamam as actions v2 com projection: "minicart" e reconciliam o drawer.

Extras sob demanda

  • useShipping() — estimate({ items, postalCode }) devolve opções de frete para o drawer.
  • useGifts() — load() devolve brindes selecionáveis / promoções (ratesAndBenefits).
  • useAttachments() — load(itemIndex) devolve os attachments de uma linha + slots oferecidos.
  • resetCart() — limpa o estado do singleton após logout ou pedido finalizado.

Opcional: adapter TanStack Query

Para sites já em @tanstack/react-query, o createCartQuery({ invoke }) de @decocms/apps-vtex/hooks/cartQuery expõe os mesmos seis hooks ligados a um QueryClient (queries lazy com enabled: false, useShipping com key + cache por staleTime). Requer um QueryClientProvider na árvore.

Wiring

  • TanStack Start — as actions v2 são declaradas no invoke do app; rode npm run generate (ou generate:invoke) para emitir os bindings createServerFn locais do site. O handler gerado chama forwardResponseCookies() para que os cookies do carrinho VTEX cheguem ao browser.
  • Next.js — loaders/actions resolvem via handleInvoke (montado em app/deco/[[...deco]]/route.ts); sem passo extra de geração. O forwarding de cookies é feito por vtexFetchWithCookies dentro de cada action.

Toda mutação de carrinho deve usar vtexFetchWithCookies. vtexFetch / vtexCachedFetch não rotacionam os cookies checkout.vtex.com / CheckoutOrderFormOwnership, o que faz o carrinho do storefront divergir do estado server-side da VTEX.


Carrinho legado (v1)

O useCart abaixo é o carrinho original. Continua funcionando e não está deprecado, mas prefira o Cart v2 para código novo — o v1 cria um OrderForm no mount para todo visitante e sempre devolve o OrderForm completo. Durante uma migração gradual, mantenha uma única fonte de verdade para o badge: não rode um badge v1 e um v2 lado a lado, ou eles divergem (os dois sistemas têm estado independente).

Import

import {
  useCart,
  useUser,
  useWishlist,
  useAutocomplete,
} from "@decocms/apps-vtex/hooks";

useCart

const {
  data: cart,
  isLoading,
  addItems,
  updateItems,
  removeItems,
  applyCoupon,
  changeRegion,
  isMutating,
} = useCart();

Estado do carrinho + mutações. Comportamento embutido:

  • Carregamento eager quando o hook monta (a menos que enabled: false).
  • Atualizações otimistas em adicionar/remover/atualizar.
  • Auto-refetch após qualquer mutação para reconciliar com o server.
  • Refetch em focus (toggle) — desabilitado por default.

Padrão de uso em um botão "adicionar ao carrinho":

function AddToCartButton({ sku }: { sku: string }) {
  const { addItems, isMutating } = useCart();
  return (
    <button
      onClick={() => addItems([{ id: sku, quantity: 1 }])}
      disabled={isMutating}
    >
      {isMutating ? "Adicionando..." : "Adicionar ao carrinho"}
    </button>
  );
}

useUser

const {
  data: user,
  isLoading,
  signIn,
  signOut,
  signUp,
} = useUser();

Estado de auth + ações.

function AccountWidget() {
  const { data: user, signOut } = useUser();
  if (!user) return <SignInLink />;
  return <span>Olá, {user.firstName} <button onClick={signOut}>Sair</button></span>;
}

useWishlist

const {
  data: wishlist,
  isLoading,
  add,
  remove,
  toggle,
  has,
} = useWishlist();

has(productId) devolve booleano sincronamente do estado em cache. toggle(productId) adiciona ou remove de acordo.

useAutocomplete

Para o input do header de busca:

const { data, isLoading } = useAutocomplete(query, { enabled: query.length >= 2 });

Devolve { products: Product[]; suggestions: string[] }.

useAutocomplete é debounced internamente (200ms) — chame em todo keystroke; ele é safe.

Por que esses hooks?

Em v1, lojas frequentemente reimplementavam useCart no zero porque o context provider do Preact era escasso. v2 traz o hook plataforma-canônico, então toda loja tem o mesmo comportamento de cache e a mesma forma de mutação otimista de graça.

Se sua loja v1 customizou useCart (e.g. para mostrar loading states diferentes), porte essa lógica como wrapper:

import { useCart as useCartBase } from "@decocms/apps-vtex/hooks";
 
export function useCart() {
  const cart = useCartBase();
  return {
    ...cart,
    isLoadingFirstTime: cart.isLoading && !cart.data,
  };
}

Legacy: factories createUse*

@decocms/apps-vtex/hooks ainda exporta:

import { createUseCart, createUseUser } from "@decocms/apps-vtex/hooks";

Eram a forma de extensibilidade no v1 (embrulhar o hook com transformação custom). Mantidas para compat de migração; prefira embrulhar os hooks novos diretamente.

TanStack Query por baixo

Todo hook usa um query key específico:

HookQuery key
useCart["vtex", "cart"]
useUser["vtex", "user"]
useWishlist["vtex", "wishlist"]
useAutocomplete["vtex", "autocomplete", query]

Use as keys para invalidar manualmente:

import { useQueryClient } from "@tanstack/react-query";
 
const qc = useQueryClient();
qc.invalidateQueries({ queryKey: ["vtex", "cart"] });

Útil quando uma mutação fora do hook (e.g. submit de form) deve refletir no carrinho.

SSR e hidratação

Hooks chamam invoke que posta para /deco/invoke. No SSR, invoke é resolvido via RequestContext (sem round trip de rede). No client, vira fetch normal.

O resultado: hooks "funcionam" em ambos os ambientes, mas dados de SSR são tipicamente mais frescos. Por isso o cliente que se hidrata pode trocar — TanStack Query trata isso via revalidação default em mount.

Hidratação de carrinho zumbi

Bug clássico: SSR vê cookie do orderForm, render cart.items.length === 3. Hidratação client-side fetcha sem o cookie (e.g. browsers terceiros bloqueando), render cart.items.length === 0. UI "atualiza" para vazio.

Mitigação:

  • Defina staleTime: 30_000 em useCart para mudança de UI ser bloqueada na primeira hidratação.
  • Em ambientes server-render-known-different, vire o hook com enabled: false e oba via useState na hidratação inicial.

@decocms/apps-vtex/utils/cookies cuida da maioria dos casos automaticamente — veja VTEX gotchas.

Veja também