S
SolidJS•2mo 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•2mo 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
ChrisThornham•2mo ago
So should I just move the onCleanup to the top of the createEffect?
Brendonovich
Brendonovich•2mo 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
ChrisThornham•2mo 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•2mo 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
ChrisThornham•2mo 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•2mo 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
ChrisThornham•2mo ago
Unfortunately, no. Same error.
Brendonovich
Brendonovich•2mo ago
ah - it'll at least avoid the race condition 😅
ChrisThornham
ChrisThornham•2mo ago
Haha
Brendonovich
Brendonovich•2mo ago
The check for the existence of checkoutElement also fails on the second load
how are you creating checkoutElement?
ChrisThornham
ChrisThornham•2mo 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•2mo 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
ChrisThornham•2mo 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•2mo ago
ChrisThornham
ChrisThornham•2mo 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•2mo 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
More Posts
how do i redirect on load?i did this ```ts export const route = { load: async () => { "use server"; const cookie = gDifficulty updating deeply nested store: produce() typed as Partial<T>I'm experiencing some difficulties updating deeply nested state within a store, specifically with thWhere can I find any documentation about `RouteDefinition`?I can see (in `dist/types.d.ts`) what properties it has, but I don't know what some of them are for.how do i use Drizzle?I'm trying to use Drizzle db on the server only and its giving me "Uncaught TypeError: promisify is Problems changing the value of a reactive signal returned from a functionI was testing what would happen if I changed the entire value of the board, not the squares but the OptionalParams in route not workingI'm not getting the expected result using optional params - routes -- users ---- [[id]].tsx ---- inReactivity resulting from updates in a websocket event listenerI am struggling to understand why setting a signal in an event listener is not driving re-rendering How do i conditionally add something to <head> based on what createAsync returns?I need to wait for a resource coming from `createAsync` so then i can decide whether i should add goHow do you delete an entry in an array when using `createStore`In the following documentation, this case is not presented: https://docs.solidjs.com/guides/complex-Uncaught ReferenceError: React is not definedHey there! Please help. I can't figure out this error. I have a tiny solid-js project that works ok