# How to implement Stripe Elements in React

- Tool: Stripe
- Difficulty: Intermediate
- Time required: 20 minutes
- Compatibility: @stripe/react-stripe-js v2+, React 18+, Stripe API v2024-12+
- Last updated: March 2026

## TL;DR

Implement Stripe Elements in React using @stripe/react-stripe-js. Wrap your app in Elements provider with a client_secret, use the PaymentElement component for the form, and call stripe.confirmPayment() via the useStripe and useElements hooks. All card data stays in Stripe's secure iframe.

## Using Stripe Elements in React Applications

The @stripe/react-stripe-js library provides React components and hooks for Stripe Elements. Instead of manually mounting DOM elements, you use the <PaymentElement /> component and the useStripe() and useElements() hooks. The Elements provider component accepts a client_secret from your PaymentIntent, and all child components can access Stripe's functionality through hooks. This is the recommended way to integrate Stripe payments in React apps.

## Before you start

- React 18+ project (Create React App, Next.js, or Vite)
- A server endpoint that creates a PaymentIntent and returns the client_secret
- Stripe publishable key (pk_test_)
- Node.js 18+ for the backend

## Step-by-step guide

### 1. Install Stripe React packages

Install both @stripe/stripe-js (the core Stripe.js loader) and @stripe/react-stripe-js (the React bindings).

```
npm install @stripe/stripe-js @stripe/react-stripe-js
```

**Expected result:** Both packages appear in your package.json dependencies.

### 2. Initialize Stripe outside your component tree

Call loadStripe() outside your component to avoid recreating the Stripe object on every render. This returns a Promise that resolves to the Stripe instance.

```
// src/stripe.ts (or .js)
import { loadStripe } from '@stripe/stripe-js';

export const stripePromise = loadStripe('pk_test_YOUR_PUBLISHABLE_KEY');
```

**Expected result:** stripePromise is a singleton Promise that resolves to the Stripe instance.

### 3. Create the payment form component

Build a CheckoutForm component that uses the useStripe and useElements hooks to handle payment confirmation.

```
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
import { useState, FormEvent } from 'react';

export function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();
  const [error, setError] = useState<string | null>(null);
  const [processing, setProcessing] = useState(false);

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    if (!stripe || !elements) return;

    setProcessing(true);
    setError(null);

    const { error: confirmError } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: window.location.origin + '/payment-complete',
      },
    });

    // Only reaches here if there's an immediate error
    if (confirmError) {
      setError(confirmError.message || 'Payment failed');
      setProcessing(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      {error && <div style={{ color: 'red', marginTop: 8 }}>{error}</div>}
      <button type="submit" disabled={!stripe || processing}>
        {processing ? 'Processing...' : 'Pay now'}
      </button>
    </form>
  );
}
```

**Expected result:** The form renders a Stripe PaymentElement with a submit button and error display.

### 4. Wrap the form in the Elements provider

Fetch the client_secret from your server, then wrap the CheckoutForm in the Elements provider. The provider passes the Stripe context to all child hooks and components.

```
import { Elements } from '@stripe/react-stripe-js';
import { stripePromise } from './stripe';
import { CheckoutForm } from './CheckoutForm';
import { useEffect, useState } from 'react';

export function PaymentPage() {
  const [clientSecret, setClientSecret] = useState('');

  useEffect(() => {
    fetch('/create-payment-intent', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ amount: 2000 }), // $20.00
    })
      .then((res) => res.json())
      .then((data) => setClientSecret(data.clientSecret));
  }, []);

  if (!clientSecret) return <div>Loading...</div>;

  return (
    <Elements stripe={stripePromise} options={{ clientSecret }}>
      <CheckoutForm />
    </Elements>
  );
}
```

**Expected result:** The PaymentPage fetches a client_secret, then renders the Elements provider with the CheckoutForm inside it.

### 5. Test the integration

Run your React app and backend. Use test card 4242 4242 4242 4242 to verify the payment flow works end to end.

```
// Test card: 4242 4242 4242 4242
// Expiry: 12/34
// CVC: 123
// The PaymentElement shows card fields + any wallet options
```

**Expected result:** The PaymentElement renders, accepts test card input, and redirects to /payment-complete on success.

## Complete code example

File: `src/CheckoutForm.tsx`

```typescript
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
import { useState, FormEvent } from 'react';

export function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();
  const [error, setError] = useState<string | null>(null);
  const [processing, setProcessing] = useState(false);
  const [succeeded, setSucceeded] = useState(false);

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    if (!stripe || !elements) return;

    setProcessing(true);
    setError(null);

    const { error: confirmError, paymentIntent } = await stripe.confirmPayment({
      elements,
      redirect: 'if_required',
    });

    if (confirmError) {
      setError(confirmError.message || 'An unexpected error occurred.');
      setProcessing(false);
    } else if (paymentIntent?.status === 'succeeded') {
      setSucceeded(true);
      setProcessing(false);
    }
  };

  if (succeeded) {
    return <div style={{ color: 'green' }}>Payment succeeded! Thank you.</div>;
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 400 }}>
      <PaymentElement
        options={{
          layout: 'tabs',
        }}
      />
      {error && (
        <div style={{ color: '#df1b41', marginTop: 8, fontSize: 14 }}>
          {error}
        </div>
      )}
      <button
        type="submit"
        disabled={!stripe || processing}
        style={{
          marginTop: 16,
          padding: '10px 24px',
          background: processing ? '#aab7c4' : '#5469d4',
          color: 'white',
          border: 'none',
          borderRadius: 4,
          fontSize: 16,
          cursor: processing ? 'not-allowed' : 'pointer',
          width: '100%',
        }}
      >
        {processing ? 'Processing...' : 'Pay $20.00'}
      </button>
    </form>
  );
}
```

## Common mistakes

- **Calling loadStripe inside a React component** — undefined Fix: Call loadStripe outside any component (e.g., in a separate file or at module level). Calling it inside a component recreates the Stripe instance on every render.
- **Rendering <PaymentElement /> without the <Elements> provider** — undefined Fix: PaymentElement and all Stripe hooks must be children of the <Elements> provider. The provider supplies the Stripe context they need.
- **Passing options without clientSecret to Elements** — undefined Fix: The Elements provider requires options={{ clientSecret }} to initialize the PaymentElement. Fetch the clientSecret from your server before rendering Elements.
- **Using the secret key in React code** — undefined Fix: React runs in the browser. Only use the publishable key (pk_test_) in frontend code. The secret key (sk_test_) belongs on your server.

## Best practices

- Call loadStripe() outside components at module level to avoid re-initialization
- Show a loading state while fetching the clientSecret from your server
- Disable the submit button while processing and when stripe/elements are not ready
- Use redirect: 'if_required' to stay on the same page for card payments
- Handle both error and success states explicitly in your component
- Use TypeScript for type safety with Stripe's types
- Set layout: 'tabs' on PaymentElement for a clean multi-method UI
- Test with 4242 4242 4242 4242 in test mode before accepting real payments

## Frequently asked questions

### Can I use Stripe Elements with Next.js?

Yes. loadStripe and the Elements provider work in Next.js. Since Stripe.js requires the browser, mark your payment component with 'use client' in the App Router. loadStripe returns null during SSR and initializes on the client.

### What is the difference between CardElement and PaymentElement?

CardElement only accepts card payments. PaymentElement automatically shows all payment methods enabled in your Stripe Dashboard (cards, wallets, bank debits, etc.). Use PaymentElement for new integrations.

### Do I need a separate backend for the PaymentIntent?

Yes. The PaymentIntent must be created server-side using your secret key. In Next.js, you can use an API route. In a React SPA, you need a separate Express or similar server.

### How do I customize the appearance of PaymentElement?

Pass an appearance object to the Elements provider options: options={{ clientSecret, appearance: { theme: 'stripe', variables: { colorPrimary: '#0570de' } } }}. You can theme colors, fonts, borders, and spacing.

### Can I get help integrating Stripe Elements into an existing React app?

RapidDev helps teams integrate Stripe into existing React applications, handling the server-side PaymentIntent creation, webhook setup, and frontend Elements integration as a complete package.

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-implement-stripe-elements-in-react
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-implement-stripe-elements-in-react
