When you asynchronously load Stripe with the StripeProvider within a component that is not the main App but mounted via a route change, lets say a 'subscribe page' all works well and Stripe will inject 2 iFrames in the Body of your HTML, (two controller script sources). You then change the route again so a different component is then mounted, lets say the 'home page', but then route back to the component where you are loading Stripe, 'subscribe page' re-rendering the StripeProvider everything is fine and working but it causes Stripe to inject more iFrames into your body. For each route change away and then back, causing another render of your component, you will get 1 more iFrame in your HTML Body.
If I don't load it asynchronously and place the StripeProvider within the main App you don't have to worry about this, but it doesn't provide the benefits of only loading when needed, if ever by a user.
Is there a way to prevent these multiple iFrame's from being injected? Otherwise I'm missing the use case for the async loading?
Hi @smschick, thanks for the issue!
Unmounting and re-mounting StripeProvider is basically the same as creating two instances of window.Stripe, such as:
const stripe1 = Stripe('pk_xxx');
const stripe2 = Stripe('pk_yyy'):
Each instance of window.Stripe has it's own controller iframe it can isolate its communication with our servers.
The best solution to your problem is to render StripeProvider above your routing logic, something like this:
const CheckoutPage = () => {
return (
<Elements>
<InjectedCheckoutForm />
</Elements>
);
}
<StripeProvider apiKey="..." >
<Router>
<Route path="/home" component={HomePage} />
<Route path="/checkout" component={CheckoutPage} />
</Router>
</StripeProvider>
This prevents the unnecessary mounting / unmounting of the component. To make sure this works with asynchronous loading, you should load and manage the Stripe instance yourself, and pass it in as <StripeProvider stripe={myAsyncLoadedStripeInstance} />.
To extend the example from the README:
class App extends React.Component {
constructor() {
super();
this.state = {stripe: null};
}
componentDidMount() {
if (window.Stripe) {
this.setState({stripe: window.Stripe('pk_test_12345')});
} else {
document.querySelector('#stripe-js').addEventListener('load', () => {
// Create Stripe instance once Stripe.js loads
this.setState({stripe: window.Stripe('pk_test_12345')});
});
}
}
render() {
// this.state.stripe will either be null or a Stripe instance
// depending on whether Stripe.js has loaded.
return (
<StripeProvider stripe={this.state.stripe}>
<Router>
<Route path="/home" component={HomePage} />
<Route path="/checkout" component={CheckoutPage} />
</Router>
</StripeProvider>
);
}
}
Does that help?
Yes and no, it kind of proves my point that you more or less have to/should place the StripeProvider in the main app or at least before the router, but it does give me an idea to use the route path on componentDidupdate to determine if a particular page/route has been switched to before asynchronously loading.
Thanks for the quick reply, much appreciated.
Actually, the better solution would be allow Stripe instances to be destroyed and garbage collected. I've found some workarounds to completely remove Stripe without reloading, but it's very very frustrating. The thing is, these persistent iframes take up a few MB each of JavaScript VM memory alone. The process memory is going to be implementation dependent, but in Chrome, the controller + metrics frames take up a total of around 80MB. That's not a ton by Chrome standards, but it's also not zero, which is what I would prefer.
Curious as to why this thread was closed, the problem is not resolved.
Can you comment on whether this is something the React Stripe team is planning to fix or won't fix?
This will better help users know whether to use this or something else completely.
@atty-stripe
Same issue here
There is some news about this huge problem? The same happens with angular, it is very bad situation. To much memory and firefox start flickering when an iframe is added!
This issue was closed because a solution was provided by atty.
To avoid creating multiple iframe, you need to avoid creating multiple Stripe instances. While this library attempts to re-use an existing Stripe instance if the key and options haven't changed, the safest is to directly inject a singleton Stripe instance.
To make asynchronous loading easier, we've created a new library, @stripe/stripe-js. You can use it with this library with a tweak to the example in the Readme:
import {loadStripe} from '@stripe/stripe-js';
class App extends React.Component {
constructor() {
super();
this.state = {stripe: null};
loadStripe('pk_test_12345').then((stripe) => {
this.setState({stripe});
});
}
render() {
// this.state.stripe will either be null or a Stripe instance
// depending on whether Stripe.js has loaded.
// It will stay null for Server-side rendering (SSR)
return (
<StripeProvider stripe={this.state.stripe}>
<Router>
<Route path="/home" component={HomePage} />
<Route path="/checkout" component={CheckoutPage} />
</Router>
</StripeProvider>
);
}
}
I'm not sure what your angular bindings do, but if you can provide a singleton stripe instance , there shouldn't be multiple iframes created in the body.
Okay, except there's still no way to destroy a Stripe instance. It's like malware. As soon as I create a Stripe instance, I'm stuck with these iframes for the duration of the session without doing some really weasel stuff. I can't imagine why you think this is okay.
Most helpful comment
Okay, except there's still no way to destroy a Stripe instance. It's like malware. As soon as I create a Stripe instance, I'm stuck with these iframes for the duration of the session without doing some really weasel stuff. I can't imagine why you think this is okay.