# How to set up Stripe with React

- Tool: Stripe
- Difficulty: Beginner
- Time required: 25 minutes
- Compatibility: React 18+, @stripe/react-stripe-js v2+, Node.js 18+
- Last updated: March 2026

## TL;DR

Set up Stripe in a React app by installing @stripe/stripe-js and @stripe/react-stripe-js, creating a backend endpoint for PaymentIntents, wrapping your app in the Elements provider, and using the PaymentElement to collect payments. Keep your secret key on the server and use only the publishable key in React.

## Full React + Stripe Setup Guide

Integrating Stripe with React requires two parts: a backend server that creates PaymentIntents using your secret key (sk_), and a React frontend that collects card details using Stripe Elements with your publishable key (pk_). This guide walks through the complete setup from installing packages to processing your first test payment. You will never handle raw card data — Stripe Elements collects it in a secure iframe.

## Before you start

- A React project (Create React App, Vite, or Next.js)
- Node.js 18+ installed for the backend server
- A Stripe account with test API keys from Dashboard → Developers → API keys
- Basic familiarity with React hooks (useState, useEffect)

## Step-by-step guide

### 1. Install frontend packages

In your React project, install the Stripe.js loader and the React bindings.

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

**Expected result:** Both packages are added to your React project's dependencies.

### 2. Set up the backend server

Create a simple Express server that handles PaymentIntent creation. This keeps your secret key off the frontend.

```
// server/index.js
const express = require('express');
const cors = require('cors');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const app = express();
app.use(cors({ origin: 'http://localhost:3000' }));
app.use(express.json());

app.post('/create-payment-intent', async (req, res) => {
  try {
    const { amount } = req.body;
    const paymentIntent = await stripe.paymentIntents.create({
      amount,
      currency: 'usd',
      automatic_payment_methods: { enabled: true },
    });
    res.json({ clientSecret: paymentIntent.client_secret });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(4000, () => console.log('Server on port 4000'));
```

**Expected result:** The server runs on port 4000 and responds to POST /create-payment-intent with a clientSecret.

### 3. Initialize Stripe in React

Create a stripe utility file that initializes Stripe once outside of React's render cycle.

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

export const stripePromise = loadStripe(
  import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_test_YOUR_KEY'
);
```

**Expected result:** stripePromise is a module-level singleton that loads Stripe.js once.

### 4. Build the payment page component

Create a page that fetches the clientSecret and wraps the checkout form in the Elements provider.

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

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

  useEffect(() => {
    fetch('http://localhost:4000/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 <p>Loading payment form...</p>;

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

**Expected result:** The page fetches the clientSecret, then renders the Stripe Elements provider.

### 5. Build the checkout form component

Create the form that renders the PaymentElement and handles submission using the useStripe and useElements hooks.

```
// src/CheckoutForm.tsx
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);

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

    if (error) {
      setError(error.message || 'Payment failed');
      setProcessing(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <button disabled={!stripe || processing}>
        {processing ? 'Processing...' : 'Pay $20.00'}
      </button>
    </form>
  );
}
```

**Expected result:** The form renders the PaymentElement and handles payment confirmation.

### 6. Test the full flow

Start both the backend (port 4000) and the React app (port 3000). Use test card 4242 4242 4242 4242 to complete a test payment.

```
// Terminal 1: STRIPE_SECRET_KEY=sk_test_xxx node server/index.js
// Terminal 2: npm start (or npm run dev)
// Test card: 4242 4242 4242 4242, Expiry: 12/34, CVC: 123
```

**Expected result:** The payment form loads, accepts the test card, and redirects to /success after payment.

## Complete code example

File: `src/App.tsx`

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

const stripePromise = loadStripe('pk_test_YOUR_PUBLISHABLE_KEY');

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 result = await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: window.location.origin + '/success',
      },
    });

    if (result.error) {
      setError(result.error.message || 'Something went wrong.');
      setProcessing(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '40px auto' }}>
      <h2>Complete your payment</h2>
      <PaymentElement />
      {error && <p style={{ color: '#df1b41', marginTop: 8 }}>{error}</p>}
      <button
        type="submit"
        disabled={!stripe || processing}
        style={{ marginTop: 16, padding: '10px 20px', width: '100%' }}
      >
        {processing ? 'Processing...' : 'Pay $20.00'}
      </button>
    </form>
  );
}

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

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

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

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

## Common mistakes

- **Putting the Stripe secret key in React code or .env without the VITE_/REACT_APP_ prefix** — undefined Fix: The secret key (sk_) must only be on your backend. The publishable key (pk_) goes in the frontend. For environment variables in React, use the framework-specific prefix (VITE_, REACT_APP_, NEXT_PUBLIC_).
- **Calling loadStripe inside a component** — undefined Fix: Call loadStripe at the module level, outside any component. Inside a component, it reinitializes on every render, causing flickering and performance issues.
- **Rendering Elements without a clientSecret** — undefined Fix: Wait for the server to return the clientSecret before rendering the Elements provider. Show a loading state while fetching.
- **Not setting up CORS on the backend** — undefined Fix: Your React dev server (port 3000) and your API server (port 4000) are different origins. Use the cors middleware with your frontend's origin.

## Best practices

- Keep the secret key (sk_) on the server only — never in React code or browser-accessible environment variables
- Use the publishable key (pk_) in React via a VITE_/REACT_APP_/NEXT_PUBLIC_ environment variable
- Initialize loadStripe at module level, not inside a component
- Show a loading state while fetching the clientSecret from your server
- Use TypeScript for better type safety with Stripe's React types
- Set up CORS on your backend to allow requests from your frontend origin
- Test end-to-end with card 4242 4242 4242 4242 in test mode
- Add a webhook endpoint on your server for payment_intent.succeeded to confirm payments

## Frequently asked questions

### Does this work with Next.js?

Yes. In the Next.js App Router, mark your payment components with 'use client'. Use NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY for the env var. For the server endpoint, use a Next.js API route or Route Handler.

### Do I need a separate backend server?

Yes, because PaymentIntents require your secret key which cannot be in the browser. With Next.js, you can use API routes as the backend. With a React SPA, you need a separate Express or similar server.

### Can I use Vite instead of Create React App?

Absolutely. The setup is the same. Use import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY for your publishable key environment variable in Vite.

### Why do I see a CORS error?

Your React dev server and API server run on different ports (different origins). Add CORS middleware to your Express server: app.use(cors({ origin: 'http://localhost:3000' })).

### How do I style the PaymentElement?

Pass an appearance option to the Elements provider: options={{ clientSecret, appearance: { theme: 'stripe' } }}. You can customize colors, fonts, and spacing through the appearance API.

### What if my Stripe + React setup is complex and I need expert help?

RapidDev can help set up production-grade Stripe integrations in React apps, including webhook handling, subscription management, and multi-page checkout flows tailored to your business requirements.

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-set-up-stripe-with-react
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-set-up-stripe-with-react
