React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

stripesetuppaymentsElementsloadStripe

Getting Started with Stripe

Recipe

Install the three Stripe packages, configure environment variables, create server and client Stripe instances, and wrap your app with the Elements provider.

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

Set up environment variables in .env.local:

STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...

Create the server-side Stripe instance:

// lib/stripe.ts
import Stripe from "stripe";
 
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2024-12-18.acacia",
  typescript: true,
});

Create the client-side Stripe loader:

// lib/stripe-client.ts
import { loadStripe } from "@stripe/stripe-js";
 
export const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

Wrap your app with the Elements provider:

// app/providers.tsx
"use client";
 
import { Elements } from "@stripe/react-stripe-js";
import { stripePromise } from "@/lib/stripe-client";
 
export function StripeProvider({ children }: { children: React.ReactNode }) {
  return <Elements stripe={stripePromise}>{children}</Elements>;
}
// app/layout.tsx
import { StripeProvider } from "./providers";
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <StripeProvider>{children}</StripeProvider>
      </body>
    </html>
  );
}

Working Example

// app/checkout/page.tsx
"use client";
 
import { useStripe, useElements } from "@stripe/react-stripe-js";
 
export default function CheckoutPage() {
  const stripe = useStripe();
  const elements = useElements();
 
  if (!stripe || !elements) {
    return <div>Loading Stripe...</div>;
  }
 
  return (
    <div>
      <h1>Checkout</h1>
      <p>Stripe loaded successfully. Ready to accept payments.</p>
      <p>
        Use test card: <code>4242 4242 4242 4242</code> with any future
        expiry and any CVC.
      </p>
    </div>
  );
}

Deep Dive

How It Works

  • The stripe npm package is the server-side Node.js SDK. It should only be imported in server code (Server Actions, Route Handlers, server components).
  • @stripe/stripe-js provides the loadStripe function that asynchronously loads the Stripe.js script from Stripe's CDN. This keeps PCI compliance simple because card data never touches your server.
  • @stripe/react-stripe-js provides React components (Elements, PaymentElement, CardElement) and hooks (useStripe, useElements) that wrap Stripe.js.
  • The Elements provider must wrap any component that uses Stripe hooks or elements. It initializes Stripe.js and makes it available via React Context.
  • loadStripe returns a Promise that resolves once the script loads. Passing this Promise directly to Elements is the recommended pattern -- it handles the async loading internally.

Variations

Lazy-load Elements only on checkout pages:

// app/checkout/layout.tsx
"use client";
 
import { Elements } from "@stripe/react-stripe-js";
import { stripePromise } from "@/lib/stripe-client";
 
export default function CheckoutLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <Elements stripe={stripePromise}>{children}</Elements>;
}

Pass a client secret to Elements for PaymentIntent flows:

<Elements
  stripe={stripePromise}
  options={{ clientSecret: "pi_xxx_secret_yyy" }}
>
  {children}
</Elements>

TypeScript Notes

  • The Stripe constructor from the stripe package is typed with the API version. Always pass an explicit apiVersion to avoid type mismatches.
  • loadStripe returns Promise<Stripe | null>. The Elements component handles the null case internally.
  • Import Stripe as a type from stripe for server-side typing, and Stripe from @stripe/stripe-js for client-side typing. They are different types.
// Server-side type
import type Stripe from "stripe";
 
// Client-side type
import type { Stripe as StripeJS } from "@stripe/stripe-js";

Gotchas

  • Never import stripe (server SDK) in client components. It exposes your secret key and will fail at build time.
  • The publishable key must be prefixed with NEXT_PUBLIC_ to be available in client-side code in Next.js.
  • loadStripe should be called outside of components (at module scope) to avoid recreating the Stripe instance on every render.
  • In test mode, all API calls use test data. No real charges are created. Switch to live keys only after thorough testing.
  • The apiVersion passed to the server SDK should match the version your Stripe Dashboard is configured for. Pinning it avoids surprises when Stripe releases new API versions.

Test Card Numbers

Card NumberScenario
4242 4242 4242 4242Successful payment
4000 0000 0000 32203D Secure authentication required
4000 0000 0000 9995Declined (insufficient funds)
4000 0000 0000 0002Generic decline
4000 0025 0000 3155Requires authentication

Use any future expiration date and any 3-digit CVC for all test cards.

Alternatives

ApproachProsCons
Global Elements providerSimple setup, works everywhereLoads Stripe.js on every page
Per-page Elements wrapperOnly loads Stripe on checkout pagesRequires wrapping each payment page
Dynamic import of ElementsSmallest initial bundleMore complex setup, slight delay on checkout

FAQs

What are the three npm packages needed for Stripe in a Next.js app, and what does each one do?
  • stripe -- server-side Node.js SDK for calling the Stripe API
  • @stripe/stripe-js -- client-side loader (loadStripe) that fetches Stripe.js from the CDN
  • @stripe/react-stripe-js -- React components (Elements, PaymentElement) and hooks (useStripe, useElements)
Why must the publishable key use the NEXT_PUBLIC_ prefix?

Next.js only exposes environment variables to client-side code if they start with NEXT_PUBLIC_. Without the prefix, the variable is undefined in the browser and loadStripe will fail silently.

What happens if you import the server-side stripe package in a client component?
  • It exposes your secret key to the browser bundle
  • The build will fail because the stripe package depends on Node.js APIs not available in the browser
  • Always keep server SDK imports in Server Actions, Route Handlers, or server components only
Why should loadStripe be called at module scope instead of inside a component?

Calling it inside a component body would re-execute on every render, creating a new Stripe instance each time. Calling at module scope ensures a single shared Promise that resolves once.

What is the purpose of the Elements provider?
  • It initializes Stripe.js and makes the Stripe instance available via React Context
  • Any component using useStripe, useElements, or Stripe Element components must be a descendant of <Elements>
  • It accepts the loadStripe Promise directly and handles async loading internally
How do you lazy-load Stripe only on checkout pages instead of globally?

Wrap only the checkout route's layout.tsx with the <Elements> provider instead of the root layout. This way Stripe.js is not loaded on pages that do not need it.

What does the apiVersion option do when creating the server-side Stripe instance?

It pins the Stripe API version your code expects. Without it, Stripe uses your Dashboard's default version, which can change and cause breaking behavior unexpectedly.

Gotcha: What is the difference between the Stripe type from stripe and the Stripe type from @stripe/stripe-js?

They are completely different types. The server-side Stripe (from stripe) types the Node SDK. The client-side Stripe (from @stripe/stripe-js) types the browser Stripe.js instance. Import them separately using import type.

import type Stripe from "stripe";             // server
import type { Stripe as StripeJS } from "@stripe/stripe-js"; // client
How do you pass a client secret to Elements for a PaymentIntent flow?
<Elements
  stripe={stripePromise}
  options={{ clientSecret: "pi_xxx_secret_yyy" }}
>
  {children}
</Elements>
What does loadStripe return and how does Elements handle the null case?

loadStripe returns Promise<Stripe | null>. It resolves to null if the script fails to load. The <Elements> component handles this internally -- hooks like useStripe will return null until loading succeeds.

TypeScript: How should you type the StripeProvider component's props?
function StripeProvider({ children }: { children: React.ReactNode }) {
  return <Elements stripe={stripePromise}>{children}</Elements>;
}

children is typed as React.ReactNode. The stripe prop on <Elements> accepts Promise<Stripe | null> | Stripe | null.

Gotcha: Can you use test mode and live mode keys in the same environment?

No. Test keys (sk_test_, pk_test_) and live keys (sk_live_, pk_live_) operate in completely separate environments. Mixing them (e.g., server uses live, client uses test) will cause API errors. Always use matching pairs.