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

How to implement Stripe Elements in React

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.

What you'll learn

  • How to install and set up @stripe/react-stripe-js
  • How to use the Elements provider with a PaymentIntent client_secret
  • How to render the PaymentElement component and handle form submission
  • How to handle errors, loading states, and payment confirmation in React
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate6 min read20 minutes@stripe/react-stripe-js v2+, React 18+, Stripe API v2024-12+March 2026RapidDev Engineering Team
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.

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

1

Install Stripe React packages

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

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

typescript
1// src/stripe.ts (or .js)
2import { loadStripe } from '@stripe/stripe-js';
3
4export 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.

typescript
1import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
2import { useState, FormEvent } from 'react';
3
4export 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
10 const handleSubmit = async (e: FormEvent) => {
11 e.preventDefault();
12 if (!stripe || !elements) return;
13
14 setProcessing(true);
15 setError(null);
16
17 const { error: confirmError } = await stripe.confirmPayment({
18 elements,
19 confirmParams: {
20 return_url: window.location.origin + '/payment-complete',
21 },
22 });
23
24 // Only reaches here if there's an immediate error
25 if (confirmError) {
26 setError(confirmError.message || 'Payment failed');
27 setProcessing(false);
28 }
29 };
30
31 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.

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.

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

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.

typescript
1// Test card: 4242 4242 4242 4242
2// Expiry: 12/34
3// CVC: 123
4// 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 working example

src/CheckoutForm.tsx
1import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
2import { useState, FormEvent } from 'react';
3
4export 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);
10
11 const handleSubmit = async (e: FormEvent) => {
12 e.preventDefault();
13 if (!stripe || !elements) return;
14
15 setProcessing(true);
16 setError(null);
17
18 const { error: confirmError, paymentIntent } = await stripe.confirmPayment({
19 elements,
20 redirect: 'if_required',
21 });
22
23 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 };
31
32 if (succeeded) {
33 return <div style={{ color: 'green' }}>Payment succeeded! Thank you.</div>;
34 }
35
36 return (
37 <form onSubmit={handleSubmit} style={{ maxWidth: 400 }}>
38 <PaymentElement
39 options={{
40 layout: 'tabs',
41 }}
42 />
43 {error && (
44 <div style={{ color: '#df1b41', marginTop: 8, fontSize: 14 }}>
45 {error}
46 </div>
47 )}
48 <button
49 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.

ChatGPT Prompt

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.

Stripe Prompt

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.

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.