S
SolidJS•7mo ago
ChrisThornham

Trouble With onCleanup()

I'm building a checkout flow with Stripe's embedded checkout form. It works like this. The user clicks a "Buy Now" button on a products page that navigates to a checkout page. The checkout page: - Gets the product ID and quantity from the search params. - Gets the checkout form from stripe - Mounts the checkout form. This works the first time I click a buy now button. BUT If I click back to the products page and click another buy now button, the checkout form doesn't load. I'm getting an error from Stripe: "You cannot have multiple embedded checkout items." So, how do I "clear" or unmount the checkout form when I click the back button? I want to start fresh each time I click a buy now button. My use of onCleanup (below) isn't working. Here's my checkout page:
export default function CheckoutPage() {

const [searchParams] = useSearchParams();

// ref
let checkoutElement: HTMLDivElement;

createEffect(async () => {
// extract the search params
const itemsArray = [
{
price: searchParams.priceId,
quantity: searchParams.qty,
},
];

// Stripe Stuff
// Create a checkout session
const response = await fetch("api/stripe/create-checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
items: itemsArray,
}),
});

// Create checkout form
const { clientSecret } = await response.json();
const checkout = await stripe?.initEmbeddedCheckout({
clientSecret,
});

// Mount form
if (checkoutElement) {
checkout?.mount(checkoutElement);
}

// unmount the form
onCleanup(() => {
checkout?.unmount();
});
});

return (
<MainLayout>
<div
id="checkout"
ref={(el) => checkoutElement = el}
class="px-6 py-20 bg-[#0f151d]"
>
{/* Checkout will insert payment form here */}
</div>
</MainLayout>
);
}
export default function CheckoutPage() {

const [searchParams] = useSearchParams();

// ref
let checkoutElement: HTMLDivElement;

createEffect(async () => {
// extract the search params
const itemsArray = [
{
price: searchParams.priceId,
quantity: searchParams.qty,
},
];

// Stripe Stuff
// Create a checkout session
const response = await fetch("api/stripe/create-checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
items: itemsArray,
}),
});

// Create checkout form
const { clientSecret } = await response.json();
const checkout = await stripe?.initEmbeddedCheckout({
clientSecret,
});

// Mount form
if (checkoutElement) {
checkout?.mount(checkoutElement);
}

// unmount the form
onCleanup(() => {
checkout?.unmount();
});
});

return (
<MainLayout>
<div
id="checkout"
ref={(el) => checkoutElement = el}
class="px-6 py-20 bg-[#0f151d]"
>
{/* Checkout will insert payment form here */}
</div>
</MainLayout>
);
}
17 Replies
Brendonovich
Brendonovich•7mo ago
i think it's because youre calling onCleanup after an await solid's reactivity system works up until await, after which point the reactive owner won't be present anymore
ChrisThornham
ChrisThornhamOP•7mo ago
So should I just move the onCleanup to the top of the createEffect?
Brendonovich
Brendonovich•7mo ago
i think that'd be the best solution yeah though you may also need to remove the async from the effect as well, since i think that will immediately wrap everything in a promise that isn't part of the reactive context
ChrisThornham
ChrisThornhamOP•7mo ago
Yeah, but I need await for the call to my API endpoint. Moving onCleanup up didn't work. The check for the existence of checkoutElement also fails on the second load:
if (checkoutElement) {
checkout?.mount(checkoutElement);
}
if (checkoutElement) {
checkout?.mount(checkoutElement);
}
Brendonovich
Brendonovich•7mo ago
i think you'd need something like this - the onCleanup needs to be called synchronously
createEffect(() => {
// extract the search params
const itemsArray = [
{
price: searchParams.priceId,
quantity: searchParams.qty,
},
];

let checkout: any;

onCleanup(() => {
checkout?.unmount();
})

(async () => {
// Stripe Stuff
// Create a checkout session
const response = await fetch("api/stripe/create-checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
items: itemsArray,
}),
});

// Create checkout form
const { clientSecret } = await response.json();
checkout = await stripe?.initEmbeddedCheckout({
clientSecret,
});

// Mount form
if (checkoutElement) {
checkout?.mount(checkoutElement);
}
})();
});
createEffect(() => {
// extract the search params
const itemsArray = [
{
price: searchParams.priceId,
quantity: searchParams.qty,
},
];

let checkout: any;

onCleanup(() => {
checkout?.unmount();
})

(async () => {
// Stripe Stuff
// Create a checkout session
const response = await fetch("api/stripe/create-checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
items: itemsArray,
}),
});

// Create checkout form
const { clientSecret } = await response.json();
checkout = await stripe?.initEmbeddedCheckout({
clientSecret,
});

// Mount form
if (checkoutElement) {
checkout?.mount(checkoutElement);
}
})();
});
though i think this could suffer a race condition where cleanup could happen before the checkout is made, and then the checkout wouldn't end being unmounted i wonder if createResource would be a better option here
ChrisThornham
ChrisThornhamOP•7mo ago
I'm not sure. I've got to run for a bit. I'll keep plugging away at this and see if anyone else chimes in with some ideas.
Brendonovich
Brendonovich•7mo ago
ah i think this will do it
createEffect(() => {
// extract the search params
const itemsArray = [
{
price: searchParams.priceId,
quantity: searchParams.qty,
},
];

const checkoutPromise = (async () => {
// Stripe Stuff
// Create a checkout session
const response = await fetch("api/stripe/create-checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
items: itemsArray,
}),
});

// Create checkout form
const { clientSecret } = await response.json();
const checkout = await stripe?.initEmbeddedCheckout({
clientSecret,
});

// Mount form
if (checkoutElement) {
checkout?.mount(checkoutElement);
}

return checkout
})();

onCleanup(() => {
checkoutPromise.then(c => c?.unmount());
})
});
createEffect(() => {
// extract the search params
const itemsArray = [
{
price: searchParams.priceId,
quantity: searchParams.qty,
},
];

const checkoutPromise = (async () => {
// Stripe Stuff
// Create a checkout session
const response = await fetch("api/stripe/create-checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
items: itemsArray,
}),
});

// Create checkout form
const { clientSecret } = await response.json();
const checkout = await stripe?.initEmbeddedCheckout({
clientSecret,
});

// Mount form
if (checkoutElement) {
checkout?.mount(checkoutElement);
}

return checkout
})();

onCleanup(() => {
checkoutPromise.then(c => c?.unmount());
})
});
ChrisThornham
ChrisThornhamOP•7mo ago
Unfortunately, no. Same error.
Brendonovich
Brendonovich•7mo ago
ah - it'll at least avoid the race condition 😅
ChrisThornham
ChrisThornhamOP•7mo ago
Haha
Brendonovich
Brendonovich•7mo ago
The check for the existence of checkoutElement also fails on the second load
how are you creating checkoutElement?
ChrisThornham
ChrisThornhamOP•7mo ago
Seems like a problem @peerreynders could solve in his sleep. Haha.
let checkoutElement: HTMLDivElement;
let checkoutElement: HTMLDivElement;
That's at least how the docs suggest to do it: https://docs.solidjs.com/concepts/refs#refs
Brendonovich
Brendonovich•7mo ago
yeah that's a fine way to do it ah i think you need to destroy not unmount
checkout.destroy() Removes Checkout from the DOM and destroys it. Once destroyed, an embedded Checkout instance cannot be reattached to the DOM. Call checkout.initEmbeddedCheckout to create a new embedded Checkout instance after unmounting the previous instance from the DOM.
ChrisThornham
ChrisThornhamOP•7mo ago
You're a genius! That works. Where did you get destroy from? A quick search in the docs and I don't see it mentioned.
Brendonovich
Brendonovich•7mo ago
ChrisThornham
ChrisThornhamOP•7mo ago
Oh!!! I was thinking destroy was from solid. Duh Great. Can't thank you enough. I was fighting this for a couple of hours. Stripe works great, but their docs are very dense. It's easy to miss things. Thanks again!
Brendonovich
Brendonovich•7mo ago
yeah there's a lot to keep track of haha, glad it's fixed for ya
Want results from more Discord servers?
Add your server