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

# React

> Complete guide to integrating the SDK with React applications

Use the base `@commercengine/storefront` package for React SPAs. Keep the SDK factory in one module, use the public accessor for catalog reads, and initialize one live session for cart, authentication, account, and order flows.

## Installation

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @commercengine/storefront @tanstack/react-query
```

For the recommended checkout experience:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @commercengine/checkout
```

## Configure the storefront

```typescript src/lib/storefront.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import {
  BrowserTokenStorage,
  Environment,
  createStorefront,
} from "@commercengine/storefront";

export const tokenStorage = new BrowserTokenStorage("brand_");

export const storefront = createStorefront({
  storeId: import.meta.env.VITE_STORE_ID,
  apiKey: import.meta.env.VITE_API_KEY,
  environment:
    import.meta.env.VITE_CE_ENV === "production"
      ? Environment.Production
      : Environment.Staging,
  session: {
    tokenStorage,
  },
});

export const publicSdk = storefront.public();
export const sdk = storefront.session();
```

The SDK instances are stable and can be imported directly. A React context is optional; do not add one merely to wrap an already shared singleton.

## Root bootstrap

Establish the anonymous session once when the application starts. Public catalog rendering does not need to wait for this call.

```tsx src/providers.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { sdk } from "./lib/storefront";

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());

  useEffect(() => {
    sdk.ensureAccessToken().catch((error) => {
      console.error("Commerce Engine session bootstrap failed", error);
    });
  }, []);

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}
```

<Info>
  `ensureAccessToken()` is safe to centralize at startup. Do not repeat it before every session method.
</Info>

## Public catalog queries

```typescript src/hooks/use-products.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { useQuery } from "@tanstack/react-query";
import { publicSdk } from "../lib/storefront";

export function useProducts(page = 1) {
  return useQuery({
    queryKey: ["products", page],
    queryFn: async () => {
      const { data, error } = await publicSdk.catalog.listProducts({
        page,
        limit: 20,
      });

      if (error) throw new Error(error.message);
      return data;
    },
  });
}
```

Use `searchProducts()` rather than `listProducts()` when the page needs facets, filtering, or search. The search response contains flattened sellable Items/SKUs.

## Hosted Checkout: recommended cart and checkout

Hosted Checkout supplies cart, login, addresses, fulfilment, discounts, payment, and confirmation. When the React app also uses the Storefront SDK, the Storefront SDK must own the session.

Update the storefront configuration to send SDK token changes to checkout:

```typescript src/lib/storefront.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { getCheckout } from "@commercengine/checkout";
import {
  BrowserTokenStorage,
  Environment,
  createStorefront,
} from "@commercengine/storefront";

export const tokenStorage = new BrowserTokenStorage("brand_");

export const storefront = createStorefront({
  storeId: import.meta.env.VITE_STORE_ID,
  apiKey: import.meta.env.VITE_API_KEY,
  environment:
    import.meta.env.VITE_CE_ENV === "production"
      ? Environment.Production
      : Environment.Staging,
  session: {
    tokenStorage,
    onTokensUpdated: (accessToken, refreshToken) => {
      getCheckout().updateTokens(accessToken, refreshToken);
    },
  },
});

export const publicSdk = storefront.public();
export const sdk = storefront.session();
```

Initialize checkout once after session bootstrap and clean it up when the root unmounts:

```tsx src/storefront-bootstrap.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { useEffect } from "react";
import {
  destroyCheckout,
  initCheckout,
} from "@commercengine/checkout/react";
import { sdk, tokenStorage } from "./lib/storefront";

export function StorefrontBootstrap() {
  useEffect(() => {
    let active = true;

    async function initialize() {
      const accessToken = await sdk.ensureAccessToken();
      const refreshToken = await tokenStorage.getRefreshToken();

      if (!active) return;

      initCheckout({
        storeId: import.meta.env.VITE_STORE_ID,
        apiKey: import.meta.env.VITE_API_KEY,
        environment:
          import.meta.env.VITE_CE_ENV === "production"
            ? "production"
            : "staging",
        authMode: "provided",
        accessToken: accessToken ?? undefined,
        refreshToken: refreshToken ?? undefined,
        onTokensUpdated: ({ accessToken, refreshToken }) => {
          void sdk.setTokens(accessToken, refreshToken);
        },
      });
    }

    initialize().catch((error) => {
      console.error("Commerce Engine checkout initialization failed", error);
    });

    return () => {
      active = false;
      destroyCheckout();
    };
  }, []);

  return null;
}
```

Mount it once inside the query provider:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
<QueryClientProvider client={queryClient}>
  <StorefrontBootstrap />
  {children}
</QueryClientProvider>
```

Then use the checkout hook in any component:

```tsx src/components/cart-button.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { useCheckout } from "@commercengine/checkout/react";

export function CartButton() {
  const { cartCount, isReady, openCart } = useCheckout();

  return (
    <button type="button" onClick={openCart} disabled={!isReady}>
      Cart ({cartCount})
    </button>
  );
}
```

```tsx src/components/add-to-cart-button.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { useCheckout } from "@commercengine/checkout/react";

export function AddToCartButton({
  productId,
  variantId,
  hasVariants,
}: {
  productId: string;
  variantId: string | null;
  hasVariants: boolean;
}) {
  const { addToCart, isReady } = useCheckout();
  const disabled = !isReady || (hasVariants && !variantId);

  return (
    <button
      type="button"
      disabled={disabled}
      onClick={() => addToCart(productId, variantId, 1)}
    >
      Add to cart
    </button>
  );
}
```

## Custom authentication UI

Build custom auth only when the storefront needs login state outside Hosted Checkout, such as account, order, loyalty, or saved-address pages.

An OTP form has two explicit steps:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data: challenge, error: loginError } = await sdk.auth.loginWithEmail({
  email,
  register_if_not_exists: true,
});

if (loginError || !challenge) {
  throw new Error(loginError?.message ?? "OTP could not be sent");
}

const { data: verified, error: verifyError } = await sdk.auth.verifyOtp({
  otp,
  otp_token: challenge.otp_token,
  otp_action: challenge.otp_action,
});

if (verifyError) throw new Error(verifyError.message);
```

After login, invalidate account and cart queries. Commerce Engine merges the active anonymous cart automatically; do not create a new cart.

Logout preserves the returned anonymous session:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { error } = await sdk.auth.logout();
if (error) throw new Error(error.message);

queryClient.removeQueries({ queryKey: ["account"] });
queryClient.invalidateQueries({ queryKey: ["cart"] });
```

Do not call `sdk.clearTokens()` after successful logout.

## Custom cart: advanced

Use a custom cart only when the storefront needs a fully custom cart/checkout UI. Hosted Checkout already handles these concerns.

### First add versus later mutations

Commerce Engine does not create an empty cart.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import type { Cart } from "@commercengine/storefront";

async function addItem(
  cart: Cart | null,
  productId: string,
  variantId: string | null,
  increment = 1,
) {
  if (increment < 1) throw new Error("Increment must be at least 1");

  if (!cart) {
    return sdk.cart.createCart({
      items: [
        {
          product_id: productId,
          variant_id: variantId,
          quantity: increment,
        },
      ],
    });
  }

  const existingLine = cart.cart_items.find(
    (item) =>
      item.product_id === productId &&
      (item.variant_id ?? null) === variantId,
  );

  const nextQuantity =
    (existingLine?.quantity ?? 0) + increment;

  return sdk.cart.addDeleteCartItem(
    { id: cart.id },
    {
      product_id: productId,
      variant_id: variantId,
      quantity: nextQuantity,
    },
  );
}
```

`addDeleteCartItem()` accepts the desired total quantity, not a delta. Call this helper inside the serialized mutation queue with the latest returned cart, so repeated Add to Cart actions increment the existing line rather than resetting it to `1`.

### Serialize cart mutations

Every mutation returns the full updated cart. Concurrent mutations can race and overwrite local state with an older response. Queue mutations with concurrency `1` or use an async mutex.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
let cartQueue: Promise<void> = Promise.resolve();

export function enqueueCartMutation(operation: () => Promise<void>) {
  cartQueue = cartQueue.then(operation, operation);
  return cartQueue;
}
```

### Query shape

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
export function useCart() {
  return useQuery({
    queryKey: ["cart"],
    queryFn: async () => {
      const { data, error, response } = await sdk.cart.getUserCart();

      if (response?.status === 404) return null;
      if (error) throw new Error(error.message);
      return data?.cart ?? null;
    },
  });
}
```

Display `cart.cart_items` and use `cart.to_be_paid` as the final amount. Check `expires_at` when restoring a persisted cart ID.

## Product detail state

For products with variants:

* use option query parameters as canonical URL state
* resolve a variant only when all option keys match its `associated_options`
* disable impossible or unavailable combinations
* keep Add to Cart disabled until the selected variant is purchasable
* pass the selected `variant.id`

For simple products, pass `variant_id: null`.

See [Catalog](/docs/storefront/catalog) and the production starters for the full model.

## Error and loading states

A production React storefront should distinguish:

* first load from background refresh
* empty result from request failure
* confirmed 404 from transient server failure
* checkout not ready from checkout error
* anonymous user from invalid session

Use query error boundaries or route-level error components, but continue checking the SDK's `{ data, error, response }` result inside each query function.

## Production checklist

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
[ ] public catalog queries do not wait for session bootstrap
[ ] one session bootstrap at the root
[ ] one QueryClient per app instance
[ ] Hosted Checkout initialized once and destroyed on unmount
[ ] async initialization cannot recreate checkout after cleanup
[ ] provided auth mode and two-way token synchronization
[ ] anonymous cart survives login and logout transitions
[ ] first cart add creates the cart
[ ] custom cart mutations are serialized
[ ] variant URL state survives refresh and sharing
[ ] typecheck, lint, test, and production build pass
```

<Card title="React production examples" icon="github" href="https://github.com/tark-ai/ce-starter-projects">
  Compare the Vite React storefronts with the Next.js and TanStack Start implementations to separate React component patterns from framework rendering patterns.
</Card>
