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
Install frontend packages
Install frontend packages
In your React project, install the Stripe.js loader and the React bindings.
1npm install @stripe/stripe-js @stripe/react-stripe-jsExpected result: Both packages are added to your React project's dependencies.
Set up the backend server
Set up the backend server
Create a simple Express server that handles PaymentIntent creation. This keeps your secret key off the frontend.
1// server/index.js2const express = require('express');3const cors = require('cors');4const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);56const app = express();7app.use(cors({ origin: 'http://localhost:3000' }));8app.use(express.json());910app.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});2324app.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.
Initialize Stripe in React
Initialize Stripe in React
Create a stripe utility file that initializes Stripe once outside of React's render cycle.
1// src/lib/stripe.ts2import { loadStripe } from '@stripe/stripe-js';34export 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.
Build the payment page component
Build the payment page component
Create a page that fetches the clientSecret and wraps the checkout form in the Elements provider.
1// src/PaymentPage.tsx2import { useState, useEffect } from 'react';3import { Elements } from '@stripe/react-stripe-js';4import { stripePromise } from './lib/stripe';5import { CheckoutForm } from './CheckoutForm';67export function PaymentPage() {8 const [clientSecret, setClientSecret] = useState('');910 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.0015 })16 .then((res) => res.json())17 .then((data) => setClientSecret(data.clientSecret));18 }, []);1920 if (!clientSecret) return <p>Loading payment form...</p>;2122 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.
Build the checkout form component
Build the checkout form component
Create the form that renders the PaymentElement and handles submission using the useStripe and useElements hooks.
1// src/CheckoutForm.tsx2import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';3import { useState, FormEvent } from 'react';45export function CheckoutForm() {6 const stripe = useStripe();7 const elements = useElements();8 const [error, setError] = useState<string | null>(null);9 const [processing, setProcessing] = useState(false);1011 const handleSubmit = async (e: FormEvent) => {12 e.preventDefault();13 if (!stripe || !elements) return;14 setProcessing(true);1516 const { error } = await stripe.confirmPayment({17 elements,18 confirmParams: { return_url: window.location.origin + '/success' },19 });2021 if (error) {22 setError(error.message || 'Payment failed');23 setProcessing(false);24 }25 };2627 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.
Test the full flow
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.
1// Terminal 1: STRIPE_SECRET_KEY=sk_test_xxx node server/index.js2// Terminal 2: npm start (or npm run dev)3// Test card: 4242 4242 4242 4242, Expiry: 12/34, CVC: 123Expected result: The payment form loads, accepts the test card, and redirects to /success after payment.
Complete working example
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';910const stripePromise = loadStripe('pk_test_YOUR_PUBLISHABLE_KEY');1112function CheckoutForm() {13 const stripe = useStripe();14 const elements = useElements();15 const [error, setError] = useState<string | null>(null);16 const [processing, setProcessing] = useState(false);1718 const handleSubmit = async (e: FormEvent) => {19 e.preventDefault();20 if (!stripe || !elements) return;21 setProcessing(true);22 setError(null);2324 const result = await stripe.confirmPayment({25 elements,26 confirmParams: {27 return_url: window.location.origin + '/success',28 },29 });3031 if (result.error) {32 setError(result.error.message || 'Something went wrong.');33 setProcessing(false);34 }35 };3637 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 <button43 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}5253export default function App() {54 const [clientSecret, setClientSecret] = useState('');5556 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 }, []);6566 if (!clientSecret) return <p>Loading...</p>;6768 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation