Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Usa useStripe y useElements de @stripe/react-stripe-js para acceder a instancias de Stripe en tus componentes. Crea hooks personalizados para encapsular patrones de pago comunes y reducir el boilerplate.
Hooks integrados:
"use client";
import { useStripe, useElements } from "@stripe/react-stripe-js";
function PaymentForm() {
// Accede a la instancia de Stripe.js
const stripe = useStripe();
// Accede a la instancia de Elements (gestiona los Elements montados)
const elements = useElements();
// Ambos devuelven null hasta que Stripe.js carga
if (!stripe || !elements) return <div>Cargando...</div>;
// Ahora puedes usar stripe.confirmPayment, elements.submit(), etc.
}Hook personalizado para el estado del pago:
// hooks/use-payment-status.ts
"use client";
import { useState, useCallback } from "react";
type PaymentStatus = "idle" | "processing" | "succeeded" | "failed";
interface UsePaymentStatusReturn {
status: PaymentStatus;
error: string | null;
setProcessing: () => void;
setSucceeded: () => void;
setFailed: (message: string) => void;
reset: () => void;
}
export function usePaymentStatus(): UsePaymentStatusReturn {
const [status, setStatus] = useState<PaymentStatus>("idle");
const [error, setError] = useState<string | null>(null);
const setProcessing = useCallback(() => {
setStatus("processing");
setError(null);
}, []);
const setSucceeded = useCallback(() => {
setStatus("succeeded");
setError(null);
}, []);
const setFailed = useCallback((message: string) => {
setStatus("failed");
setError(message);
}, []);
const reset = useCallback(() => {
setStatus("idle");
setError(null);
}, []);
return { status, error, setProcessing, setSucceeded, setFailed, reset };
}Hook personalizado para el flujo completo de checkout:
// hooks/use-checkout.ts
"use client";
import { useCallback } from "react";
import { useStripe, useElements } from "@stripe/react-stripe-js";
import { usePaymentStatus } from "./use-payment-status";
interface UseCheckoutOptions {
returnUrl: string;
onSuccess?: () => void;
}
export function useCheckout({ returnUrl, onSuccess }: UseCheckoutOptions) {
const stripe = useStripe();
const elements = useElements();
const { status, error, setProcessing, setSucceeded, setFailed, reset } =
usePaymentStatus();
const handlePayment = useCallback(async () => {
if (!stripe || !elements) return;
setProcessing();
const { error: submitError } = await elements.submit();
if (submitError) {
setFailed(submitError.message ?? "La validación falló");
return;
}
const { error: confirmError, paymentIntent } =
await stripe.confirmPayment({
elements,
confirmParams: { return_url: returnUrl },
redirect: "if_required",
});
if (confirmError) {
setFailed(confirmError.message ?? "El pago falló");
} else if (paymentIntent?.status === "succeeded") {
setSucceeded();
onSuccess?.();
}
}, [stripe, elements, returnUrl, onSuccess, setProcessing, setFailed, setSucceeded]);
return {
handlePayment,
status,
error,
isReady: !!stripe && !!elements,
isProcessing: status === "processing",
reset,
};
}// app/checkout/checkout-form.tsx
"use client";
import { type FormEvent } from "react";
import { PaymentElement } from "@stripe/react-stripe-js";
import { useCheckout } from "@/hooks/use-checkout";
export function CheckoutForm({ amount }: { amount: number }) {
const {
handlePayment,
status,
error,
isReady,
isProcessing,
} = useCheckout({
returnUrl: `${window.location.origin}/success`,
onSuccess: () => {
console.log("Payment succeeded!");
},
});
function onSubmit(e: FormEvent) {
e.preventDefault();
handlePayment();
}
if (status === "succeeded") {
return (
<div className="text-center p-8">
<h2 className="text-2xl font-bold text-green-600">
¡Pago exitoso!
</h2>
<p className="text-gray-600 mt-2">
Gracias por tu compra.
</p>
</div>
);
}
return (
<form onSubmit={onSubmit} className="max-w-md mx-auto p-6 space-y-6">
<PaymentElement />
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-700 text-sm">{error}</p>
</div>
)}
<button
type="submit"
disabled={!isReady || isProcessing}
className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium
hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed
transition-colors"
>
{isProcessing
? "Procesando..."
: `Pagar $${(amount / 100).toFixed(2)}`}
</button>
</form>
);
}Hook personalizado para el estado de suscripción:
// hooks/use-subscription-status.ts
"use client";
import { useState, useEffect } from "react";
interface SubscriptionInfo {
plan: string;
status: string;
currentPeriodEnd: string | null;
}
export function useSubscriptionStatus() {
const [subscription, setSubscription] = useState<SubscriptionInfo | null>(
null
);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchStatus() {
try {
const res = await fetch("/api/subscription/status");
if (res.ok) {
const data = await res.json();
setSubscription(data);
}
} catch {
console.error("Error al obtener el estado de la suscripción");
} finally {
setLoading(false);
}
}
fetchStatus();
}, []);
const isActive =
subscription?.status === "active" ||
subscription?.status === "trialing";
const isPastDue = subscription?.status === "past_due";
const isCanceled = subscription?.status === "canceled";
return { subscription, loading, isActive, isPastDue, isCanceled };
}Uso en un panel:
// app/dashboard/page.tsx
"use client";
import { useSubscriptionStatus } from "@/hooks/use-subscription-status";
export default function DashboardPage() {
const { subscription, loading, isActive, isPastDue } =
useSubscriptionStatus();
if (loading) return <div>Cargando...</div>;
return (
<div>
{isPastDue && (
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-4">
<p className="text-yellow-800">
Tu pago está vencido. Actualiza tu método de pago.
</p>
</div>
)}
{isActive ? (
<h1>¡Bienvenido de nuevo! Estás en el plan {subscription?.plan}.</h1>
) : (
<h1>Mejora tu plan para acceder a funciones premium.</h1>
)}
</div>
);
}useStripe() devuelve la instancia de Stripe del proveedor Elements más cercano. Devuelve null hasta que Stripe.js termina de cargar de forma asíncrona.useElements() devuelve la instancia de Elements que gestiona todos los Stripe Elements montados (PaymentElement, CardElement, etc.). También devuelve null hasta que esté listo.<Elements>. Llamarlos fuera lanza un error.useCheckout componen los hooks integrados con gestión de state para crear patrones de pago reutilizables. Esto mantiene los componentes de formulario enfocados en la presentación.redirect: "if_required" en confirmPayment te permite manejar el resultado en el cliente para pagos con tarjeta, a la vez que sigue admitiendo redirecciones de 3D Secure cuando hace falta.Hook con reintento automático:
// hooks/use-payment-retry.ts
"use client";
import { useState, useCallback } from "react";
import { useStripe, useElements } from "@stripe/react-stripe-js";
export function usePaymentRetry(maxRetries = 2) {
const stripe = useStripe();
const elements = useElements();
const [retryCount, setRetryCount] = useState(0);
const confirmWithRetry = useCallback(
async (returnUrl: string) => {
if (!stripe || !elements) return { error: "No está listo" };
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
confirmParams: { return_url: returnUrl },
redirect: "if_required",
});
if (!error) return { paymentIntent };
if (error.type !== "api_error" || attempt === maxRetries) {
return { error: error.message };
}
setRetryCount(attempt + 1);
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
}
return { error: "Se superó el máximo de reintentos" };
},
[stripe, elements, maxRetries]
);
return { confirmWithRetry, retryCount };
}useStripe() devuelve Stripe | null (de @stripe/stripe-js).useElements() devuelve StripeElements | null.import type { Stripe, StripeElements } from "@stripe/stripe-js";
// Los hooks integrados devuelven tipos anulables
const stripe: Stripe | null = useStripe();
const elements: StripeElements | null = useElements();useStripe y useElements devuelven null durante la carga inicial. Comprueba siempre que no sean null antes de usarlos en manejadores de eventos.<Elements>. Usarlos fuera lanzará un error en tiempo de ejecución.stripe ni elements y los pases a closures que se ejecuten más tarde. La referencia puede quedar obsoleta. Accede siempre a ellos dentro del callback.<Elements>. Este requisito se propaga hacia arriba a través de la composición.onSuccess en hooks personalizados debe envolverse en useCallback por el consumidor para evitar recrear el callback interno del hook en cada renderizado.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Hooks integrados (useStripe, useElements) | API sencilla y oficial | Verboso en cada componente |
| Hooks personalizados de envoltura | Reutilizables, encapsulan la lógica | Abstracción adicional que mantener |
| Patrón render-prop | Funciona con componentes de clase | Verboso, patrón obsoleto |
| Stripe.js directo (sin React) | Sin dependencia de React | Gestión manual del DOM |
Ambos devuelven null. Debes comprobar siempre que no sean null antes de usarlos en manejadores de eventos o lógica de UI. Muestra un estado de carga mientras sean null.
Leen las instancias de Stripe y Elements del Context de React proporcionado por <Elements>. Llamarlos fuera lanza un error en tiempo de ejecución. Este requisito se propaga a cualquier hook personalizado que los envuelva.
"idle" | "processing" | "succeeded" | "failed"setProcessing, setSucceeded, setFailed, reset) para actualizar el stateCombina useStripe, useElements y usePaymentStatus en un solo hook que devuelve handlePayment, status, error, isReady e isProcessing. Un componente de formulario puede llamar a handlePayment() al enviar sin gestionar la lógica de Stripe directamente.
Indica a confirmPayment que solo redirija para métodos de pago que lo requieran (como 3D Secure). Para pagos simples con tarjeta, el resultado se maneja en el cliente, lo que te permite mostrar un mensaje de éxito sin redirección de página.
Las referencias pueden quedar obsoletas cuando el closure se ejecute. Accede siempre a stripe y elements dentro del cuerpo del callback, o asegúrate de que el closure capture la referencia más reciente mediante un useCallback con las dependencias correctas.
Sin useCallback, la función onSuccess se recrea en cada renderizado, lo que provoca que el callback interno handlePayment de useCheckout también se recree en cada renderizado (ya que onSuccess está en su array de dependencias).
const { subscription, loading, isActive, isPastDue, isCanceled } =
useSubscriptionStatus();Obtiene datos de suscripción de /api/subscription/status al montar y deriva flags booleanos (isActive, isPastDue, isCanceled) del campo status.
useStripe() y useElements() devuelven null hasta que el script del CDN carga. En condiciones de red deficientes esto puede tardar varios segundos. Muestra siempre un estado de carga significativo, no una pantalla en blanco.
import type { Stripe, StripeElements } from "@stripe/stripe-js";
const stripe: Stripe | null = useStripe();
const elements: StripeElements | null = useElements();Ambos son anulables hasta que Stripe.js se inicializa.
maxRetries intentos ante fallos de tipo api_error1000 * (attempt + 1) ms)card_error) se devuelven de inmediatoRevisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥