Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to set up Stripe with React

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.

What you'll learn

  • How to set up both the React frontend and Node.js backend for Stripe
  • How to correctly separate publishable (pk_) and secret (sk_) keys
  • How to create a complete payment flow from form to confirmation
  • How to structure your React app with the Stripe Elements provider
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner6 min read25 minutesReact 18+, @stripe/react-stripe-js v2+, Node.js 18+March 2026RapidDev Engineering Team
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.

Prerequisites

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

typescript
1npm 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.

typescript
1// server/index.js
2const express = require('express');
3const cors = require('cors');
4const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
5
6const app = express();
7app.use(cors({ origin: 'http://localhost:3000' }));
8app.use(express.json());
9
10app.post('/create-payment-intent', async (req, res) => {
11 try {
12 const { amount } = req.body;
13 const paymentIntent = await stripe.paymentIntents.create({
14 amount,
15 currency: 'usd',
16 automatic_payment_methods: { enabled: true },
17 });
18 res.json({ clientSecret: paymentIntent.client_secret });
19 } catch (err) {
20 res.status(500).json({ error: err.message });
21 }
22});
23
24app.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.

typescript
1// src/lib/stripe.ts
2import { loadStripe } from '@stripe/stripe-js';
3
4export const stripePromise = loadStripe(
5 import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_test_YOUR_KEY'
6);

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.

typescript
1// src/PaymentPage.tsx
2import { useState, useEffect } from 'react';
3import { Elements } from '@stripe/react-stripe-js';
4import { stripePromise } from './lib/stripe';
5import { CheckoutForm } from './CheckoutForm';
6
7export function PaymentPage() {
8 const [clientSecret, setClientSecret] = useState('');
9
10 useEffect(() => {
11 fetch('http://localhost:4000/create-payment-intent', {
12 method: 'POST',
13 headers: { 'Content-Type': 'application/json' },
14 body: JSON.stringify({ amount: 2000 }), // $20.00
15 })
16 .then((res) => res.json())
17 .then((data) => setClientSecret(data.clientSecret));
18 }, []);
19
20 if (!clientSecret) return <p>Loading payment form...</p>;
21
22 return (
23 <Elements stripe={stripePromise} options={{ clientSecret }}>
24 <CheckoutForm />
25 </Elements>
26 );
27}

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.

typescript
1// src/CheckoutForm.tsx
2import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
3import { useState, FormEvent } from 'react';
4
5export function CheckoutForm() {
6 const stripe = useStripe();
7 const elements = useElements();
8 const [error, setError] = useState<string | null>(null);
9 const [processing, setProcessing] = useState(false);
10
11 const handleSubmit = async (e: FormEvent) => {
12 e.preventDefault();
13 if (!stripe || !elements) return;
14 setProcessing(true);
15
16 const { error } = await stripe.confirmPayment({
17 elements,
18 confirmParams: { return_url: window.location.origin + '/success' },
19 });
20
21 if (error) {
22 setError(error.message || 'Payment failed');
23 setProcessing(false);
24 }
25 };
26
27 return (
28 <form onSubmit={handleSubmit}>
29 <PaymentElement />
30 {error && <p style={{ color: 'red' }}>{error}</p>}
31 <button disabled={!stripe || processing}>
32 {processing ? 'Processing...' : 'Pay $20.00'}
33 </button>
34 </form>
35 );
36}

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.

typescript
1// Terminal 1: STRIPE_SECRET_KEY=sk_test_xxx node server/index.js
2// Terminal 2: npm start (or npm run dev)
3// 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 working example

src/App.tsx
1import { useState, useEffect, FormEvent } from 'react';
2import { loadStripe } from '@stripe/stripe-js';
3import {
4 Elements,
5 PaymentElement,
6 useStripe,
7 useElements,
8} from '@stripe/react-stripe-js';
9
10const stripePromise = loadStripe('pk_test_YOUR_PUBLISHABLE_KEY');
11
12function CheckoutForm() {
13 const stripe = useStripe();
14 const elements = useElements();
15 const [error, setError] = useState<string | null>(null);
16 const [processing, setProcessing] = useState(false);
17
18 const handleSubmit = async (e: FormEvent) => {
19 e.preventDefault();
20 if (!stripe || !elements) return;
21 setProcessing(true);
22 setError(null);
23
24 const result = await stripe.confirmPayment({
25 elements,
26 confirmParams: {
27 return_url: window.location.origin + '/success',
28 },
29 });
30
31 if (result.error) {
32 setError(result.error.message || 'Something went wrong.');
33 setProcessing(false);
34 }
35 };
36
37 return (
38 <form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '40px auto' }}>
39 <h2>Complete your payment</h2>
40 <PaymentElement />
41 {error && <p style={{ color: '#df1b41', marginTop: 8 }}>{error}</p>}
42 <button
43 type="submit"
44 disabled={!stripe || processing}
45 style={{ marginTop: 16, padding: '10px 20px', width: '100%' }}
46 >
47 {processing ? 'Processing...' : 'Pay $20.00'}
48 </button>
49 </form>
50 );
51}
52
53export default function App() {
54 const [clientSecret, setClientSecret] = useState('');
55
56 useEffect(() => {
57 fetch('http://localhost:4000/create-payment-intent', {
58 method: 'POST',
59 headers: { 'Content-Type': 'application/json' },
60 body: JSON.stringify({ amount: 2000 }),
61 })
62 .then((r) => r.json())
63 .then((data) => setClientSecret(data.clientSecret));
64 }, []);
65
66 if (!clientSecret) return <p>Loading...</p>;
67
68 return (
69 <Elements stripe={stripePromise} options={{ clientSecret }}>
70 <CheckoutForm />
71 </Elements>
72 );
73}

Common mistakes when setting up Stripe with React

Why it's a problem: Putting the Stripe secret key in React code or .env without the VITE_/REACT_APP_ prefix

How to avoid: 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_).

Why it's a problem: Calling loadStripe inside a component

How to avoid: Call loadStripe at the module level, outside any component. Inside a component, it reinitializes on every render, causing flickering and performance issues.

Why it's a problem: Rendering Elements without a clientSecret

How to avoid: Wait for the server to return the clientSecret before rendering the Elements provider. Show a loading state while fetching.

Why it's a problem: Not setting up CORS on the backend

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Set up a complete Stripe payment integration with React. Include: 1) A Node.js Express server that creates PaymentIntents and returns the client_secret. 2) A React app with loadStripe, Elements provider, PaymentElement, and a CheckoutForm using useStripe/useElements hooks. Use TypeScript.

Stripe Prompt

Set up Stripe payments in my React app. Create: 1) A backend /create-payment-intent endpoint that returns a client_secret. 2) A React PaymentPage that fetches the client_secret and wraps a CheckoutForm in the Stripe Elements provider. 3) The CheckoutForm with PaymentElement and error handling.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.