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

# React integration guide

> Integrate Primer Checkout with React 18 and React 19 applications.

Primer Checkout uses Web Components, which work natively in React. This guide covers React-specific patterns for both React 18 and React 19.

<Note>
  **Using Next.js?** See the [SSR Guide](/checkout/primer-checkout/frameworks/ssr-guide) first—you'll need to load Primer on the client side only.
</Note>

## Quick start

Here's a minimal working example for React 19:

```tsx theme={"dark"}
import { useEffect } from 'react';
import { loadPrimer } from '@primer-io/primer-js';

// Define options outside component for stable reference
const SDK_OPTIONS = { locale: 'en-GB' };

export function Checkout({ clientToken }: { clientToken: string }) {
  useEffect(() => {
    loadPrimer();
  }, []);

  return (
    <primer-checkout
      client-token={clientToken}
      options={SDK_OPTIONS}
    />
  );
}
```

For React 18, you need a ref to pass the `options` object. See [React 18 Pattern](#react-18-pattern) below.

## TypeScript setup

TypeScript doesn't recognize custom web component tags by default. Add this declaration to your project:

```typescript theme={"dark"}
// src/types/primer.d.ts
import type { CheckoutElement } from '@primer-io/primer-js';

declare global {
  namespace JSX {
    interface IntrinsicElements {
      'primer-checkout': CheckoutElement;
    }
  }
}
```

Without this, you'll see "Property 'primer-checkout' does not exist" errors.

<Accordion title="Alternative: Import all Primer component types">
  For projects using multiple Primer components:

  ```typescript theme={"dark"}
  import { CustomElements } from '@primer-io/primer-js/dist/jsx/index';

  declare module 'react' {
    namespace JSX {
      interface IntrinsicElements extends CustomElements {}
    }
  }
  ```
</Accordion>

## React 18 vs React 19

The key difference is how you pass object properties to web components.

<Tabs>
  <Tab title="React 19">
    React 19 passes objects directly as properties:

    ```tsx theme={"dark"}
    const SDK_OPTIONS = { locale: 'en-GB' };

    function Checkout({ clientToken }: { clientToken: string }) {
      return (
        <primer-checkout
          client-token={clientToken}
          options={SDK_OPTIONS}
        />
      );
    }
    ```
  </Tab>

  <Tab title="React 18">
    React 18 converts objects to `[object Object]` strings. Use a ref instead:

    ```tsx theme={"dark"}
    import { useRef, useEffect } from 'react';

    const SDK_OPTIONS = { locale: 'en-GB' };

    function Checkout({ clientToken }: { clientToken: string }) {
      const checkoutRef = useRef<PrimerCheckoutComponent>(null);

      useEffect(() => {
        if (checkoutRef.current) {
          checkoutRef.current.options = SDK_OPTIONS;
        }
      }, []);

      return (
        <primer-checkout
          ref={checkoutRef}
          client-token={clientToken}
        />
      );
    }
    ```
  </Tab>
</Tabs>

| Aspect                             | React 18        | React 19          |
| ---------------------------------- | --------------- | ----------------- |
| How to pass `options`              | ref + useEffect | JSX prop directly |
| String attributes (`client-token`) | Works normally  | Works normally    |

## Handling payment events

Listen for payment events to handle success and failure:

<Tabs>
  <Tab title="React 19">
    ```tsx theme={"dark"}
    import { useEffect, useRef } from 'react';

    const SDK_OPTIONS = { locale: 'en-GB' };

    function Checkout({ clientToken }: { clientToken: string }) {
      const checkoutRef = useRef<PrimerCheckoutComponent>(null);

      useEffect(() => {
        const checkout = checkoutRef.current;
        if (!checkout) return;

        const handleSuccess = (event: PrimerEvents['primer:payment-success']) => {
          const { payment } = event.detail;
          console.log('Payment successful:', payment.id);
          window.location.href = `/confirmation?order=${payment.orderId}`;
        };

        const handleFailure = (event: PrimerEvents['primer:payment-failure']) => {
          const { error } = event.detail;
          console.error('Payment failed:', error.message);
          // Error is displayed in checkout UI automatically
        };

        checkout.addEventListener('primer:payment-success', handleSuccess);
        checkout.addEventListener('primer:payment-failure', handleFailure);

        return () => {
          checkout.removeEventListener('primer:payment-success', handleSuccess);
          checkout.removeEventListener('primer:payment-failure', handleFailure);
        };
      }, []);

      return (
        <primer-checkout
          ref={checkoutRef}
          client-token={clientToken}
          options={SDK_OPTIONS}
        />
      );
    }
    ```
  </Tab>

  <Tab title="React 18">
    ```tsx theme={"dark"}
    import { useEffect, useRef } from 'react';

    const SDK_OPTIONS = { locale: 'en-GB' };

    function Checkout({ clientToken }: { clientToken: string }) {
      const checkoutRef = useRef<PrimerCheckoutComponent>(null);

      useEffect(() => {
        const checkout = checkoutRef.current;
        if (!checkout) return;

        // Set options (React 18 requires this)
        checkout.options = SDK_OPTIONS;

        const handleSuccess = (event: PrimerEvents['primer:payment-success']) => {
          const { payment } = event.detail;
          console.log('Payment successful:', payment.id);
          window.location.href = `/confirmation?order=${payment.orderId}`;
        };

        const handleFailure = (event: PrimerEvents['primer:payment-failure']) => {
          const { error } = event.detail;
          console.error('Payment failed:', error.message);
        };

        checkout.addEventListener('primer:payment-success', handleSuccess);
        checkout.addEventListener('primer:payment-failure', handleFailure);

        return () => {
          checkout.removeEventListener('primer:payment-success', handleSuccess);
          checkout.removeEventListener('primer:payment-failure', handleFailure);
        };
      }, []);

      return (
        <primer-checkout
          ref={checkoutRef}
          client-token={clientToken}
        />
      );
    }
    ```
  </Tab>
</Tabs>

## Stable object references

Define `options` objects outside your component or use `useMemo`. This avoids unnecessary work on every render.

```tsx theme={"dark"}
// ✅ Good: Constant outside component
const SDK_OPTIONS = { locale: 'en-GB' };

function Checkout() {
  return <primer-checkout options={SDK_OPTIONS} />;
}

// ✅ Good: useMemo for dynamic values
function Checkout({ userLocale }: { userLocale: string }) {
  const options = useMemo(() => ({
    locale: userLocale,
  }), [userLocale]);

  return <primer-checkout options={options} />;
}

// ⚠️ Avoid: Inline objects (creates new reference every render)
function Checkout() {
  return <primer-checkout options={{ locale: 'en-GB' }} />;
}
```

<Info>
  The SDK uses deep comparison, so inline objects won't break functionality. But stable references are still recommended to avoid comparison overhead on every render.
</Info>

## Common patterns

### Show loading while checkout initializes

```tsx theme={"dark"}
import { useState, useEffect, useRef } from 'react';

const SDK_OPTIONS = { locale: 'en-GB' };

function Checkout({ clientToken }: { clientToken: string }) {
  const [isReady, setIsReady] = useState(false);
  const checkoutRef = useRef<PrimerCheckoutComponent>(null);

  useEffect(() => {
    const checkout = checkoutRef.current;
    if (!checkout) return;

    const handleReady = () => setIsReady(true);
    checkout.addEventListener('primer:ready', handleReady);
    return () => checkout.removeEventListener('primer:ready', handleReady);
  }, []);

  return (
    <div>
      {!isReady && <div className="loading-spinner">Loading checkout...</div>}
      <primer-checkout
        ref={checkoutRef}
        client-token={clientToken}
        options={SDK_OPTIONS}
        style={{ display: isReady ? 'block' : 'none' }}
      />
    </div>
  );
}
```

### Fetch client token from server

```tsx theme={"dark"}
import { useState, useEffect } from 'react';
import { loadPrimer } from '@primer-io/primer-js';

const SDK_OPTIONS = { locale: 'en-GB' };

function CheckoutPage() {
  const [clientToken, setClientToken] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    loadPrimer();

    async function fetchToken() {
      try {
        const response = await fetch('/api/create-client-session', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            amount: 1000,
            currency: 'GBP',
          }),
        });
        const data = await response.json();
        setClientToken(data.clientToken);
      } catch (err) {
        setError('Failed to initialize checkout');
      }
    }

    fetchToken();
  }, []);

  if (error) return <div className="error">{error}</div>;
  if (!clientToken) return <div>Loading...</div>;

  return (
    <primer-checkout
      client-token={clientToken}
      options={SDK_OPTIONS}
    />
  );
}
```

### Checkout in a modal

```tsx theme={"dark"}
import { useEffect, useRef } from 'react';

const SDK_OPTIONS = { locale: 'en-GB' };

interface CheckoutModalProps {
  clientToken: string;
  isOpen: boolean;
  onClose: () => void;
  onSuccess: (payment: any) => void;
}

function CheckoutModal({ clientToken, isOpen, onClose, onSuccess }: CheckoutModalProps) {
  const checkoutRef = useRef<PrimerCheckoutComponent>(null);

  useEffect(() => {
    if (!isOpen) return;

    const checkout = checkoutRef.current;
    if (!checkout) return;

    const handleSuccess = (event: PrimerEvents['primer:payment-success']) => {
      const { payment } = event.detail;
      onSuccess(payment);
      onClose();
    };

    const handleFailure = (event: PrimerEvents['primer:payment-failure']) => {
      const { error } = event.detail;
      console.error('Payment failed:', error.message);
    };

    checkout.addEventListener('primer:payment-success', handleSuccess);
    checkout.addEventListener('primer:payment-failure', handleFailure);

    return () => {
      checkout.removeEventListener('primer:payment-success', handleSuccess);
      checkout.removeEventListener('primer:payment-failure', handleFailure);
    };
  }, [isOpen, onClose, onSuccess]);

  if (!isOpen) return null;

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button className="modal-close" onClick={onClose}>×</button>
        <primer-checkout
          ref={checkoutRef}
          client-token={clientToken}
          options={SDK_OPTIONS}
        />
      </div>
    </div>
  );
}
```

### Custom hook (optional)

<Info>
  This pattern is **not required**. It's provided for teams who prefer encapsulating logic in reusable hooks. The examples above work perfectly without it.
</Info>

Encapsulate checkout logic in a reusable hook:

```tsx theme={"dark"}
import { useRef, useEffect, useState, useCallback } from 'react';

interface UsePrimerCheckoutOptions {
  onSuccess?: (payment: any) => void;
  onFailure?: (error: any) => void;
}

function usePrimerCheckout(options: UsePrimerCheckoutOptions = {}) {
  const checkoutRef = useRef<PrimerCheckoutComponent>(null);
  const [isReady, setIsReady] = useState(false);
  const [isProcessing, setIsProcessing] = useState(false);

  useEffect(() => {
    const checkout = checkoutRef.current;
    if (!checkout) return;

    const handleReady = () => {
      setIsReady(true);
    };

    const handleSuccess = (event: PrimerEvents['primer:payment-success']) => {
      const { payment } = event.detail;
      setIsProcessing(false);
      options.onSuccess?.(payment);
    };

    const handleFailure = (event: PrimerEvents['primer:payment-failure']) => {
      const { error } = event.detail;
      setIsProcessing(false);
      options.onFailure?.(error);
    };

    const handleStateChange = (event: PrimerEvents['primer:state-change']) => {
      setIsProcessing(event.detail.isProcessing);
    };

    checkout.addEventListener('primer:ready', handleReady);
    checkout.addEventListener('primer:payment-success', handleSuccess);
    checkout.addEventListener('primer:payment-failure', handleFailure);
    checkout.addEventListener('primer:state-change', handleStateChange);

    return () => {
      checkout.removeEventListener('primer:ready', handleReady);
      checkout.removeEventListener('primer:payment-success', handleSuccess);
      checkout.removeEventListener('primer:payment-failure', handleFailure);
      checkout.removeEventListener('primer:state-change', handleStateChange);
    };
  }, [options.onSuccess, options.onFailure]);

  return { checkoutRef, isReady, isProcessing };
}

// Usage
function Checkout({ clientToken }: { clientToken: string }) {
  const { checkoutRef, isReady, isProcessing } = usePrimerCheckout({
    onSuccess: (payment) => {
      window.location.href = `/confirmation?order=${payment.orderId}`;
    },
    onFailure: (error) => {
      console.error('Payment failed:', error.message);
    },
  });

  return (
    <div>
      {!isReady && <div>Loading checkout...</div>}
      {isProcessing && <div>Processing payment...</div>}
      <primer-checkout
        ref={checkoutRef}
        client-token={clientToken}
        options={SDK_OPTIONS}
      />
    </div>
  );
}
```

## Complete example

A production-ready checkout component with all the patterns combined:

```tsx theme={"dark"}
import { useEffect, useRef, useState, useMemo } from 'react';
import { loadPrimer } from '@primer-io/primer-js';

// Types
interface CheckoutProps {
  amount: number;
  currency: string;
  locale?: string;
  onSuccess?: (payment: any) => void;
  onFailure?: (error: any) => void;
}

// Load Primer SDK once at module level
loadPrimer();

export function Checkout({
  amount,
  currency,
  locale = 'en-GB',
  onSuccess,
  onFailure,
}: CheckoutProps) {
  const checkoutRef = useRef<PrimerCheckoutComponent>(null);
  const [clientToken, setClientToken] = useState<string | null>(null);
  const [isReady, setIsReady] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Stable options reference - only changes when locale changes
  const options = useMemo(() => ({ locale }), [locale]);

  // Fetch client token
  useEffect(() => {
    async function fetchToken() {
      try {
        const response = await fetch('/api/create-client-session', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ amount, currency }),
        });

        if (!response.ok) throw new Error('Failed to create session');

        const data = await response.json();
        setClientToken(data.clientToken);
      } catch (err) {
        setError('Unable to initialize checkout. Please try again.');
      }
    }

    fetchToken();
  }, [amount, currency]);

  // Set up event handlers
  useEffect(() => {
    const checkout = checkoutRef.current;
    if (!checkout || !clientToken) return;

    // React 18: Set options via ref
    checkout.options = options;

    const handleReady = () => {
      setIsReady(true);
    };

    const handleSuccess = (event: CustomEvent) => {
      const { payment } = event.detail;
      onSuccess?.(payment);
    };

    const handleFailure = (event: CustomEvent) => {
      const { error } = event.detail;
      onFailure?.(error);
    };

    checkout.addEventListener('primer:ready', handleReady);
    checkout.addEventListener('primer:payment-success', handleSuccess);
    checkout.addEventListener('primer:payment-failure', handleFailure);

    return () => {
      checkout.removeEventListener('primer:ready', handleReady);
      checkout.removeEventListener('primer:payment-success', handleSuccess);
      checkout.removeEventListener('primer:payment-failure', handleFailure);
    };
  }, [clientToken, options, onSuccess, onFailure]);

  // Error state
  if (error) {
    return (
      <div className="checkout-error">
        <p>{error}</p>
        <button onClick={() => window.location.reload()}>Try Again</button>
      </div>
    );
  }

  // Loading state
  if (!clientToken) {
    return <div className="checkout-loading">Preparing checkout...</div>;
  }

  return (
    <div className="checkout-container">
      {!isReady && <div className="checkout-loading">Loading payment methods...</div>}
      <primer-checkout
        ref={checkoutRef}
        client-token={clientToken}
        style={{ display: isReady ? 'block' : 'none' }}
      />
    </div>
  );
}
```

Usage:

```tsx theme={"dark"}
function PaymentPage() {
  return (
    <Checkout
      amount={2999}
      currency="GBP"
      locale="en-GB"
      onSuccess={(payment) => {
        window.location.href = `/order/${payment.orderId}/confirmation`;
      }}
      onFailure={(error) => {
        console.error('Payment failed:', error.diagnosticsId);
      }}
    />
  );
}
```

## Quick reference

| Scenario                | Solution                                  |
| ----------------------- | ----------------------------------------- |
| Static options          | Constant outside component                |
| Options depend on props | `useMemo` with dependencies               |
| React 18 object props   | Use ref + useEffect                       |
| React 19 object props   | Pass directly in JSX                      |
| Handle payment success  | Listen for `primer:payment-success` event |
| Track loading state     | Listen for `primer:ready` event           |
| Track processing state  | Listen for `primer:state-change` event    |

## See also

<CardGroup cols={2}>
  <Card title="SSR Guide" icon="server" href="/checkout/primer-checkout/frameworks/ssr-guide">
    Next.js, Nuxt, and SvelteKit patterns
  </Card>

  <Card title="SDK Options" icon="gear" href="/checkout/primer-checkout/configuration/sdk-options">
    All configuration options
  </Card>

  <Card title="Events Guide" icon="bolt" href="/checkout/primer-checkout/configuration/events">
    Complete event reference
  </Card>

  <Card title="Styling" icon="palette" href="/checkout/primer-checkout/build-your-ui/styling-customization">
    Customize the appearance
  </Card>
</CardGroup>
