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
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.