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 desections.
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
projection | O que chega ao browser | Quando usar |
|---|---|---|
"none" | { ok: true } | Update puramente otimista, sem reconciliação |
"summary" | { orderFormId, totalItems, total } | Só atualizar o badge |
"summary+items" | summary + itens enxutos | Padrão do add-to-cart (toast) |
"minicart" | Minicart canônico completo | Abrir o drawer |
"raw" | OrderForm cru da VTEX | GTM / 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
invokedo app; rodenpm run generate(ougenerate:invoke) para emitir os bindingscreateServerFnlocais do site. O handler gerado chamaforwardResponseCookies()para que os cookies do carrinho VTEX cheguem ao browser. - Next.js — loaders/actions resolvem via
handleInvoke(montado emapp/deco/[[...deco]]/route.ts); sem passo extra de geração. O forwarding de cookies é feito porvtexFetchWithCookiesdentro 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:
| Hook | Query 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_000emuseCartpara mudança de UI ser bloqueada na primeira hidratação. - Em ambientes server-render-known-different, vire o hook com
enabled: falsee oba viauseStatena hidratação inicial.
@decocms/apps-vtex/utils/cookies cuida da maioria dos casos automaticamente — veja VTEX gotchas.