javascriptreactjsnext.jsstripe-paymentsmedusajs

Stripe payment form constantly re-renders using @stripe/react-stripe-js


I have some issues setting up Stripe payment in my Medusa-JS Next JS frontend.

When entering card details in the checkoutForm the entire StripePayment re-renders all the time when clicking on anything inside my form that is not the PaymentElement .

I have a suspicion that it is due to changes in my Stripe component’s useEffect but I cannot see why it should be updated inside my form.

My code

StripePayment - component

"use client";
import { useState, useEffect } from "react";
import { loadStripe } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js";
import CheckoutForm from "./form";

const STRIPE_PK_KEY = process.env.NEXT_PUBLIC_STRIPE_PK_KEY;

const StripePayment = ({ cart }) => {
  const [stripePromise, setStripePromise] = useState(null);
  const [clientSecret, setClientSecret] = useState();

  useEffect(() => {
    if (!STRIPE_PK_KEY && !cart && !cart.payment_sessions) {
      return null;
    }
    const isStripeAvailable = cart.payment_sessions?.some(
      (session) => session.provider_id === "stripe"
    );
    if (!isStripeAvailable) {
      return;
    }
    setStripePromise(loadStripe(STRIPE_PK_KEY));
    setClientSecret(cart.payment_session.data.client_secret);
  }, [cart.id]);

  return (
    <div>
      <h1>Stripe payment</h1>
      {stripePromise && clientSecret && (
        <Elements stripe={stripePromise} options={{ clientSecret }}>
          <CheckoutForm clientSecret={clientSecret} cart={cart} />
        </Elements>
      )}
    </div>
  );
};

export default StripePayment;

CheckoutForm - component

"use client";
import {
  useElements,
  useStripe,
  PaymentElement,
} from "@stripe/react-stripe-js";
import { useState } from "react";
import medusaClient from "../../../lib/medusaClient";

export default function Form({ clientSecret, cart }) {
  const stripe = useStripe();
  const elements = useElements();

  const [message, setMessage] = useState("");
  const [isProcessing, setIsProcessing] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (!stripe || !elements) {
      console.log("No tripe or elements");
      return;
    }

    setIsProcessing(true);

    const { error, paymentIntent } = await stripe.confirmCardPayment(
      clientSecret,
      {
        payment_method: {
          card: elements.getElement(PaymentElement),
          billing_details: {
            name: e.target.name.value,
            email: e.target.email.value,
            phone: e.target.phone.value,
            address: {
              city: cart.shipping_address.city,
              country: cart.shipping_address.country,
              line1: cart.shipping_address.line1,
              line2: cart.shipping_address.line2,
              postal_code: cart.shipping_address.postal_code,
            },
          },
        },
      }
    );

    if (error) {
      setMessage(error.message);
    } else if (paymentIntent && paymentIntent.status === "succeeded") {
      setMessage("Payment succeeded");
      try {
        await medusaClient.carts.complete(cart.id);
        console.log("Order completed");
      } catch (err) {
        console.error("Error completing order:", err);
      }
    } else {
      setMessage("An unexpected error occurred");
    }
    console.log("Message: ", message);
    setIsProcessing(false);
  };

  return (
    <form id="payment-form" onSubmit={(e) => handleSubmit(e)}>
            {/* INPUT LABELS OMITTED FOR SAKE OF READIBILITY */}
      <PaymentElement id="payment-element" />
      <button
        className="px-4 py-2 mt-4 text-white bg-blue-500"
        disabled={isProcessing || !stripe || !elements}
        id="submit"
      >
        <span id="button-text">
          {isProcessing ? "Processing ... " : "Pay now"}
        </span>
      </button>
      {/* Show any error or success messages */}
      {message && <div id="payment-message">{message}</div>}
    </form>
  );
}

Solution

  • There's some unusual things in your code I see:

    The normal integration is more like this:

    Elements stripe={stripePromise} options={{ clientSecret }}>
      <CheckoutForm cart={cart} />
    </Elements>
    

    CheckoutForm:

    ...
     const { error } = await stripe.confirmPayment({
          elements, // from useElements ; you do not need to pass the clientSecret 
          confirmParams: {
            // Make sure to change this to your payment completion page
            return_url: `${window.location.origin}/completion`,
            payment_method_data: {
                    billing_details:{
                      name: e.target.name.value,
                      email: e.target.email.value, 
                      ...
                    }
            }
          },
        });
    ...
    

    https://github.com/stripe-samples/accept-a-payment/tree/504ffe70e4ac54a21c2c8ce3b5c426c12df6f351/payment-element/client/react-cra/src

    https://stripe.com/docs/payments/accept-a-payment?platform=web&ui=elements&client=react#web-collect-payment-details

    I would try to rewrite your code to match Stripe's docs and see if that has an impact.