> ## Documentation Index
> Fetch the complete documentation index at: https://quickbutik.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# React setup

> Install the React layer of @quickbutik/kit, wrap your app in ShopkitProvider and decide where the client lives.

`@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.

```bash theme={null}
npm install @quickbutik/kit
```

React is an optional peer dependency (`^18.2 || ^19`). Nothing else is installed: the kit has no runtime dependencies.

```ts theme={null}
import { ShopkitProvider, useCart, ProductProvider, SEO } from "@quickbutik/kit/react"
```

<Note>
  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](#using-your-own-data-layer).
</Note>

## 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 `/react` imports below a `"use client"` boundary, and
* fetch catalog data on the server with the plain client from `@quickbutik/kit`, then hand it down.

The [Next.js guide](/kit/react/nextjs) shows the full split.

## `<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.

<CodeGroup>
  ```tsx With a config theme={null}
  "use client"
  import { ShopkitProvider } from "@quickbutik/kit/react"

  export function Providers({ children }: { children: React.ReactNode }) {
    return (
      <ShopkitProvider
        config={{ publishableKey: process.env.NEXT_PUBLIC_QUICKBUTIK_PUBLISHABLE_KEY! }}
      >
        {children}
      </ShopkitProvider>
    )
  }
  ```

  ```tsx With a client you built theme={null}
  import { createShopkitClient } from "@quickbutik/kit"
  import { ShopkitProvider } from "@quickbutik/kit/react"

  const shopkit = createShopkitClient({
    publishableKey: import.meta.env.VITE_QUICKBUTIK_PUBLISHABLE_KEY,
  })

  export function App() {
    return (
      <ShopkitProvider client={shopkit}>
        <Routes />
      </ShopkitProvider>
    )
  }
  ```
</CodeGroup>

The client and the cart store are created **once** and kept for the provider's lifetime. `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`.

<ParamField path="config" type="ShopkitConfig">
  The same options as [`createShopkitClient`](/kit/reference/client). Only `publishableKey` is required.
</ParamField>

<ParamField path="client" type="ShopkitClient">
  A client you built yourself. Use instead of `config`.
</ParamField>

<ParamField path="initialCart" type="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.
</ParamField>

<ParamField path="storefrontId" type="string | null">
  Sell the whole tree into one [campaign storefront](/kit/concepts/campaign-storefronts). `null` or `""` means not set.
</ParamField>

<ParamField path="currency" type="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](#currencies).
</ParamField>

<ParamField path="consent" type="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](#consent).
</ParamField>

### Server-read cart: correct on first paint

```tsx theme={null}
// a server component / loader
const cart = await shopkit.cart.current().catch(() => null)

return (
  <ShopkitProvider config={config} initialCart={cart}>
    {children}
  </ShopkitProvider>
)
```

Use `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

```tsx theme={null}
<ShopkitProvider config={config} storefrontId="sf_01JBQ8ZK4M7XW9YR2TCVN3H5PD">
  {children}
</ShopkitProvider>
```

Product reads come back at campaign prices, carts are created bound to the campaign and the checkout is attributed to it.

* **Changing `storefrontId` rebuilds 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. `initialCart` applies 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 a `ShopkitConfigError`. Build it with `createShopkitClient({ storefrontId })`, or pass `config` instead.
* A `config` whose own `storefrontId` names a different campaign also throws, rather than silently picking one.

### Consent

```tsx theme={null}
<ShopkitProvider
  config={config}
  consent={{ initialState: stateFromTheServer, lang: "sv", privacyPolicyUrl: "/integritetspolicy" }}
>
  <App />
  <footer><ConsentSettingsButton /></footer>
</ShopkitProvider>
```

`<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.

| To | Do |
| - | - |
| Turn consent and analytics off entirely | `consent: false` in the client config (`createShopkitClient` or `<ShopkitProvider config>`), which the server client reads too |
| Keep the banner, load no pixels | `consent={{ analytics: false }}` |
| Render your own banner | `consent={{ banner: false }}`, or a `<ConsentBanner>` of your own anywhere below |
| Drop the built-in stylesheet | `consent={{ unstyled: true }}` |

<Note>
  `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.
</Note>

A provider nested inside another (a campaign `storefrontId` provider inside the site's) reuses the outer consent store, banner and analytics hub. Everything else is in [Consent and analytics](/kit/concepts/consent-and-analytics).

### Currencies

```tsx theme={null}
<ShopkitProvider config={config} currency="EUR">
```

`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:

```tsx theme={null}
import { useCurrency } from "@quickbutik/kit/react"

const { currency, currencies, mode, chargeCurrency, setCurrency } = useCurrency()
```

A switch reprices everything below the provider: catalog hooks refetch, `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](/kit/concepts/currencies) for display and charge currencies and server rendering.

### Reading the context

```tsx theme={null}
import { useShopkit, useShopkitContext } from "@quickbutik/kit/react"

const shopkit = useShopkit()                      // the client
const { client, cartStore } = useShopkitContext() // client + shared cart store
```

Both throw a named `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:

| Feature | React 19 | React 18 |
| - | - | - |
| `<ProductProvider slug>` / `id` / a promise | Unwrapped with `use()`, suspends | Throws a clear error. Resolve the product yourself and pass `product={product}` |
| `<ProductImage productId>` / `slug` lookups | Suspend with `use()` | Pass a resolved `product` or `image` |
| `<SEO />` | Hoists `<title>`, `<meta>`, `<link>` into `<head>` | Renders inline; use `useSeo()` with your framework's head mechanism |

## Vite SPA (React, no server)

<Steps>
  <Step title="Scaffold and install">
    ```bash theme={null}
    npm create vite@latest my-shop -- --template react-ts
    cd my-shop
    npm install @quickbutik/kit
    echo 'VITE_QUICKBUTIK_PUBLISHABLE_KEY=qb_pk_…' > .env.local
    ```
  </Step>

  <Step title="One client per app">
    ```tsx src/main.tsx theme={null}
    import { createShopkitClient } from "@quickbutik/kit"
    import { ShopkitProvider } from "@quickbutik/kit/react"
    import { createRoot } from "react-dom/client"
    import { App } from "./App"

    const shopkit = createShopkitClient({
      publishableKey: import.meta.env.VITE_QUICKBUTIK_PUBLISHABLE_KEY,
    })

    createRoot(document.getElementById("root")!).render(
      <ShopkitProvider client={shopkit}>
        <App />
      </ShopkitProvider>,
    )
    ```

    In the browser, storage resolves to `document.cookie`, so the cart survives reloads.
  </Step>

  <Step title="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()`.
  </Step>

  <Step title="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`).
  </Step>
</Steps>

<Warning>
  A pure SPA is not reliably indexable. `<SEO />` writes meta tags on React 19, but crawlers that do not run JavaScript see the empty shell. Prerender or render on a server when catalog SEO matters.
</Warning>

## 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 keep `ShopkitProvider` only for `useCart` / `useCheckout`:

```tsx theme={null}
import { useShopkit, useShopkitContext } from "@quickbutik/kit/react"
import type { AddCartItemInput } from "@quickbutik/kit"
import { useMutation, useQuery } from "@tanstack/react-query"

function useCatalog(cursor?: string) {
  const shopkit = useShopkit()
  return useQuery({
    queryKey: ["products", cursor],
    queryFn: ({ signal }) => shopkit.products.list({ limit: 24, cursor, signal }),
  })
}

function useAddToCart() {
  const { client, cartStore } = useShopkitContext()
  return useMutation({
    mutationFn: (item: AddCartItemInput) => client.cart.add(item),
    // Adopt the returned cart so every useCart() consumer stays in sync.
    onSuccess: (cart) => cartStore.hydrate(cart),
  })
}
```

`CartStore.hydrate(cart)` adopts a cart fetched elsewhere, so badges and drawers built on `useCart()` update without a second request.

## Next steps

<CardGroup cols={2}>
  <Card title="Catalog and products" icon="shirt" href="/kit/react/catalog-and-products">
    Hooks, ProductProvider and the variant picker.
  </Card>

  <Card title="Cart and checkout" icon="cart-shopping" href="/kit/react/cart-and-checkout">
    useCart, useCheckout, inline checkout and the thank-you page.
  </Card>

  <Card title="Next.js App Router" icon="server" href="/kit/react/nextjs">
    The reference architecture with server components and server actions.
  </Card>

  <Card title="React API reference" icon="book" href="/kit/reference/react">
    Every hook and component.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.