@quickbutik/kit/react is a set of hooks and headless components over the same framework-free core as the rest of the kit. It renders no markup of its own (the one deliberate exception is <ProductImage />, which renders a single <img>), ships no CSS and adds no wrapper elements. Your app owns 100% of the DOM.
^18.2 || ^19). Nothing else is installed: the kit has no runtime dependencies.
There is no data-fetching library underneath. State lives in a small observable store read through
useSyncExternalStore. If your app already uses TanStack Query or SWR, see Using your own data layer.Client components only
Every export from@quickbutik/kit/react is a client hook or client component; the entry point carries a "use client" directive. In a framework with server components (Next.js App Router, React Router RSC):
- keep
/reactimports below a"use client"boundary, and - fetch catalog data on the server with the plain client from
@quickbutik/kit, then hand it down.
<ShopkitProvider>
The provider makes one client and one shared cart store available to the tree. Every component that calls useCart() reads the same store, so a header badge and a cart page stay in sync without either owning the other.
config is read from a ref, not a memo dependency, so an inline object literal is fine. A config crosses the server/client boundary as a plain object (a client cannot be serialized), which is why server frameworks pass config rather than client.
ShopkitConfig
The same options as
createShopkitClient. Only publishableKey is required.ShopkitClient
A client you built yourself. Use instead of
config.Cart | null
A cart already read on the server. The store starts in
ready with it, so server render and first client paint agree and the badge never flashes from 0 to 3.string | null
Sell the whole tree into one campaign storefront.
null or "" means not set.string | null
Shorthand for
config.currency: the currency to browse in until the shopper picks one. Changing it later switches the same client with setCurrency(). See Currencies.ShopkitConsentOptions
Options for the cookie consent banner and the shop’s analytics, which are on by default:
initialState (the server’s read, so the banner never flashes), lang, privacyPolicyUrl, labels, analytics, banner, unstyled and the consent store options. To turn consent off, set consent: false in the config, not here. See Consent.Server-read cart: correct on first paint
cart.current() here, never ensure() or add(): current() reads and never creates, so a first-time visitor or a crawler causes no write.
Campaign storefronts
- Changing
storefrontIdrebuilds the client and the cart store. A campaign cart is priced differently and remembered under its own cookie, so moving between campaign pages must not keep the first campaign’s basket. Ids compare case-insensitively.initialCartapplies to the first client only. - With a prebuilt
client, the prop can only confirm that client’s binding. A client bound to a different campaign (or to none) throws aShopkitConfigError. Build it withcreateShopkitClient({ storefrontId }), or passconfiginstead. - A
configwhose ownstorefrontIdnames a different campaign also throws, rather than silently picking one.
Consent
<ShopkitProvider> shows a cookie banner, keeps the shopper’s decision in a qb_consent cookie, and loads the merchant’s own GA4, GTM and Meta pixel (from the shop’s settings) only after the shopper agrees. The product page, search, the cart and the thank-you page report commerce events into it with no code of yours. <ConsentSettingsButton> reopens the dialog later.
consent={false} on the provider still turns consent off for that React tree, but it is deprecated and logs a warning: a server-started checkout cannot see a prop, so it would still forward a decision. Use consent: false in the config instead.storefrontId provider inside the site’s) reuses the outer consent store, banner and analytics hub. Everything else is in Consent and analytics.
Currencies
currency is the currency to browse in until the shopper picks one; leave it out to start in the shop’s own currency. useCurrency() lists what the shop offers and switches:
useCart() re-reads the same cart, and the next checkout opens in the new currency. Nothing is rebuilt. The value at mount becomes the client’s default, so changing the prop to null later returns to that value rather than to the shop’s own currency. See Currencies for display and charge currencies and server rendering.
Reading the context
ShopkitConfigError when no provider is above, so the failure points at the offending component.
React 18 vs React 19
Everything works on React 18 except two things that rely on React 19:Vite SPA (React, no server)
1
Scaffold and install
2
One client per app
src/main.tsx
document.cookie, so the cart survives reloads.3
Routes
Catalog with
useProducts / useProductSearch, product pages with <ProductProvider slug> (React 19), checkout with useCheckout().redirectToCheckout, and a thank-you route at /success/:orderNumber with useOrderConfirmation().4
SPA fallback on the host
The hosted checkout returns with a top-level GET to
/success/<orderNumber>, so the host must serve index.html for unknown paths (Netlify/Cloudflare Pages _redirects: /* /index.html 200, Vercel rewrites, nginx try_files $uri /index.html).Using your own data layer
The hooks exist so a data library is not mandatory, not to replace one. With TanStack Query or SWR, call the client directly and keepShopkitProvider only for useCart / useCheckout:
CartStore.hydrate(cart) adopts a cart fetched elsewhere, so badges and drawers built on useCart() update without a second request.
Next steps
Catalog and products
Hooks, ProductProvider and the variant picker.
Cart and checkout
useCart, useCheckout, inline checkout and the thank-you page.
Next.js App Router
The reference architecture with server components and server actions.
React API reference
Every hook and component.