pealdevDOCS
Back to peal.dev

STRIPE STARTER · CHECKOUTBUTTON

CheckoutButton

The client component that takes a price ID, calls the Server Action, and redirects to Stripe. Handles loading state, error display, and accessibility automatically.

src/components/checkout/CheckoutButton.tsx is the only thing you wire to your CTA. It calls createCheckoutSession, manages the in-flight state via useTransition, shows an inline error if the action fails, and redirects on success. You pass a price ID and a label — that's it.

Usage

TSX
import { CheckoutButton } from "@/components/checkout/CheckoutButton";

<CheckoutButton priceId="price_1AbCd..." variant="default">
  Subscribe Now
</CheckoutButton>

What it handles for you

  • Loading state — disabled button + spinner while the Server Action is pending.
  • Error display — inline <p role="alert"> for screen readers, with the error text from the action.
  • Variants — "default" | "outline" | "secondary" — match the rest of the shadcn/ui buttons.
  • Redirect — window.location.href = result.url (full navigation, not router.push — Stripe URLs are off-domain).

What you DON'T do

  • Don't fetch the URL in a useEffect. The Server Action call IS the fetch.
  • Don't router.push() the Stripe URL. Use window.location — Stripe is a different origin and Next.js client routing will refuse.
  • Don't try to render Stripe Elements inside the button. That's a different integration; this template uses hosted Checkout.

Why useTransition over useState for loading

useTransition keeps the UI responsive — the button shows a spinner without blocking other state updates on the page. It's also the React-idiomatic way to wrap Server Action calls. Don't replace it with a manual setIsLoading.

Extending the button

Need analytics on click? Wrap the component or pass a callback. Don't add tracking into CheckoutButton itself — it's meant to stay a thin, reusable primitive.