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.
Prerequisites
- 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
Install Stripe React packages
Install Stripe React packages
Install both @stripe/stripe-js (the core Stripe.js loader) and @stripe/react-stripe-js (the React bindings).
1npm install @stripe/stripe-js @stripe/react-stripe-jsExpected result: Both packages appear in your package.json dependencies.
Initialize Stripe outside your component tree
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.
1// src/stripe.ts (or .js)2import { loadStripe } from '@stripe/stripe-js';34export const stripePromise = loadStripe('pk_test_YOUR_PUBLISHABLE_KEY');Expected result: stripePromise is a singleton Promise that resolves to the Stripe instance.
Create the payment form component
Create the payment form component
Build a CheckoutForm component that uses the useStripe and useElements hooks to handle payment confirmation.
1import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';2import { useState, FormEvent } from 'react';34export function CheckoutForm() {5 const stripe = useStripe();6 const elements = useElements();7 const [error, setError] = useState<string | null>(null);8 const [processing, setProcessing] = useState(false);910 const handleSubmit = async (e: FormEvent) => {11 e.preventDefault();12 if (!stripe || !elements) return;1314 setProcessing(true);15 setError(null);1617 const { error: confirmError } = await stripe.confirmPayment({18 elements,19 confirmParams: {20 return_url: window.location.origin + '/payment-complete',21 },22 });2324 // Only reaches here if there's an immediate error25 if (confirmError) {26 setError(confirmError.message || 'Payment failed');27 setProcessing(false);28 }29 };3031 return (32 <form onSubmit={handleSubmit}>33 <PaymentElement />34 {error && <div style={{ color: 'red', marginTop: 8 }}>{error}</div>}35 <button type="submit" disabled={!stripe || processing}>36 {processing ? 'Processing...' : 'Pay now'}37 </button>38 </form>39 );40}Expected result: The form renders a Stripe PaymentElement with a submit button and error display.
Wrap the form in the Elements provider
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.
1import { Elements } from '@stripe/react-stripe-js';2import { stripePromise } from './stripe';3import { CheckoutForm } from './CheckoutForm';4import { useEffect, useState } from 'react';56export function PaymentPage() {7 const [clientSecret, setClientSecret] = useState('');89 useEffect(() => {10 fetch('/create-payment-intent', {11 method: 'POST',12 headers: { 'Content-Type': 'application/json' },13 body: JSON.stringify({ amount: 2000 }), // $20.0014 })15 .then((res) => res.json())16 .then((data) => setClientSecret(data.clientSecret));17 }, []);1819 if (!clientSecret) return <div>Loading...</div>;2021 return (22 <Elements stripe={stripePromise} options={{ clientSecret }}>23 <CheckoutForm />24 </Elements>25 );26}Expected result: The PaymentPage fetches a client_secret, then renders the Elements provider with the CheckoutForm inside it.
Test the integration
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.
1// Test card: 4242 4242 4242 42422// Expiry: 12/343// CVC: 1234// The PaymentElement shows card fields + any wallet optionsExpected result: The PaymentElement renders, accepts test card input, and redirects to /payment-complete on success.
Complete working example
1import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';2import { useState, FormEvent } from 'react';34export function CheckoutForm() {5 const stripe = useStripe();6 const elements = useElements();7 const [error, setError] = useState<string | null>(null);8 const [processing, setProcessing] = useState(false);9 const [succeeded, setSucceeded] = useState(false);1011 const handleSubmit = async (e: FormEvent) => {12 e.preventDefault();13 if (!stripe || !elements) return;1415 setProcessing(true);16 setError(null);1718 const { error: confirmError, paymentIntent } = await stripe.confirmPayment({19 elements,20 redirect: 'if_required',21 });2223 if (confirmError) {24 setError(confirmError.message || 'An unexpected error occurred.');25 setProcessing(false);26 } else if (paymentIntent?.status === 'succeeded') {27 setSucceeded(true);28 setProcessing(false);29 }30 };3132 if (succeeded) {33 return <div style={{ color: 'green' }}>Payment succeeded! Thank you.</div>;34 }3536 return (37 <form onSubmit={handleSubmit} style={{ maxWidth: 400 }}>38 <PaymentElement39 options={{40 layout: 'tabs',41 }}42 />43 {error && (44 <div style={{ color: '#df1b41', marginTop: 8, fontSize: 14 }}>45 {error}46 </div>47 )}48 <button49 type="submit"50 disabled={!stripe || processing}51 style={{52 marginTop: 16,53 padding: '10px 24px',54 background: processing ? '#aab7c4' : '#5469d4',55 color: 'white',56 border: 'none',57 borderRadius: 4,58 fontSize: 16,59 cursor: processing ? 'not-allowed' : 'pointer',60 width: '100%',61 }}62 >63 {processing ? 'Processing...' : 'Pay $20.00'}64 </button>65 </form>66 );67}Common mistakes when implementing Stripe Elements in React
Why it's a problem: Calling loadStripe inside a React component
How to avoid: 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.
Why it's a problem: Rendering <PaymentElement /> without the <Elements> provider
How to avoid: PaymentElement and all Stripe hooks must be children of the <Elements> provider. The provider supplies the Stripe context they need.
Why it's a problem: Passing options without clientSecret to Elements
How to avoid: The Elements provider requires options={{ clientSecret }} to initialize the PaymentElement. Fetch the clientSecret from your server before rendering Elements.
Why it's a problem: Using the secret key in React code
How to avoid: 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
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Write a React TypeScript component that integrates Stripe Elements. Use @stripe/react-stripe-js with loadStripe, Elements provider, PaymentElement, and useStripe/useElements hooks. Fetch a PaymentIntent client_secret from /create-payment-intent and handle form submission with error states.
Add Stripe payment to my React app. Create a PaymentPage component that fetches a client_secret from the server, wraps a CheckoutForm in the Stripe Elements provider, and uses PaymentElement with useStripe/useElements hooks. Handle loading, error, and success states.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation