-
Notifications
You must be signed in to change notification settings - Fork 319
Stripe async example and README doesn't always load properly #154
Comments
I added a check that |
Hey @ljbade, thanks for reaching out! But I'm a little confused. Are you suggesting that this change should be made somewhere in our code? It looks rather similar to this example code from the README: class App extends React.Component {
constructor() {
this.state = {stripe: null};
}
componentDidMount() {
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}>
<Elements>
<InjectedCheckoutForm />
</Elements>
</StripeProvider>
);
}
} The difference is that instead of running in the It might help me to understand if you could put together a JSFiddle that reproduces the issue you mention running into. |
@jez The problem is that Stripe could already be loaded by the time |
Ah, okay. Good catch! In that case, would this change work? diff --git a/app.js b/app.js
index e5ebb35..110de30 100644
--- a/app.js
+++ b/app.js
@@ -3,11 +3,15 @@ class App extends React.Component {
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. This is the relevant section of the docs for using
If you really want to avoid drawing out the first render, you can do the check in a |
I updated the original comment with the final code we have that works properly with SSR. |
@jez perfect thats the same code we ended up going with. |
It's actually a little different. It's an anti-pattern to call |
I think that actually the best solution is to use dynamic script tag injection, like this: But I've opened up a PR which makes the change to |
Ran into this issue with Stripe.js too. Worked most of the time, minus when the I did exactly what Jez recommended above re: setTimeout(function(){ Solved it perfectly. |
here’s a single Provider component I just wrote to load the stripe.js script asynchronously, which I based on that demo code: AsyncStripeProvider.js |
Does anyone have advice on how to handle the situation in which Stripe fails to download (for whatever reason). Before stripe-elements I used this (admittedly complicated) technique to force all operations that used Stripe to either wait for the download to complete or handle the error. It's unclear to me how I can do this using the I was hoping that stripe elements would handle all this complicated stuff for me, but after switching to the simplified version we're still hitting this error in production:
|
I wrapped StripeProvider in my own components and do the dynamic script loading inside of componentDidMount. This allowed me to hook my react callback functions up to the // componentDidMount
if (!window.Stripe) {
const stripeJs = document.createElement('script');
stripeJs.src = 'https://js.stripe.com/v3/';
stripeJs.async = true;
stripeJs.onload = () => {
this.props.onLoad();
this.setState({
stripe: window.Stripe(this.props.stripePublicKey),
});
};
stripeJs.onerror = (err) => {
this.props.onError();
this.setState({
error: 'Something went wrong trying to load our credit card form. Please try again later',
});
};
document.body.appendChild(stripeJs);
}
// render
return <StripeProvider stripe={this.state.stripe}>
<Elements>
{this.props.children}
</Elements>
</StripeProvider>; I also wrapped the /// render
<CardElement
onReady={this.props.onReady}
/> |
@wichopy ah, interesting. I've put my StripeProvider on the root mount so that it's available to the whole app (I believe it's advised to load stripe on every page load for fraud-detection reasons). So I guess here i'd have to wrap the StripeProvider in my own custom provider that provides both the error (or null) and the stripe instance (or null). |
@rhys-vdw Yep you can set it up so that whenever you have a CardForm you wrap it in the custom provider. I did not put it in the root mount to save a bit on initial load times on pages that don't need a credit card form. Its a bit more work putting the Provider in multiple pages but it has been working out for us so far. |
FWIW - I'm using
I didn't need to mess with state or handle any callback. The user has to take a couple actions before our payment modal appears, so I don't have any issues with the library not being loaded by the time the user needs it. |
This is the only thing that have helped me. I am using state hooks in a simple component, I did almost everything and nothing worked but this. |
Hey guys, I loved this, but I like using Hooks even more. Here's my version of this component using hooks: https://gist.github.com/daviseford/4a0ed622dc47090fe22c1870217d88d6 My version also protects against adding the stripe script more than once to the document! |
Perhaps this bit oh, code also needs a removeEventListener...
Incorrect
Also incorrect :-). Dont do direct state mutation. |
all of the shown solutions will continually create new instances of stripe on the window, resulting iframes stacking up in the browser. In our app, we only want the stripe instance to mount for certain routes, so we wrapped all of our Billing routes in a catchall route that mounts the stripe provider and then mounts our billing routes like so: <Route path='/billing' render={()=>(
<AsyncStripeProvider apiKey={config.stripeKey}>
<Elements>
<Route path='/billing/payment-method' exact component={PaymentMethod} />
</Elements>
</AsyncStripeProvider>
)} /> We quickly discover that everytime we enter a billing page, a new Stripe Iframe is added, because the AsyncStripeProvider will create a new instance everytime it is mount. This is the workaround we came with ( fork of code by @mrcoles ): import React, { Component } from 'react';
import { StripeProvider } from 'react-stripe-elements';
export default class AsyncStripeProvider extends Component {
state = {
stripe: null
}
// life-cycle
componentDidMount() {
this._mounted = true;
const stripeJsElement = document.getElementById('my-stripe-js');
if(!stripeJsElement) {
const stripeJs = document.createElement('script');
stripeJs.id = 'my-stripe-js';
stripeJs.src = 'https://js.stripe.com/v3/';
stripeJs.async = true;
stripeJs.onload = () => {
if (this._mounted) {
if(window._$stripe_instance===undefined){
window._$stripe_instance = window.Stripe(this.props.apiKey)
}
this.setState({
stripe: window._$stripe_instance
});
}
};
document.body && document.body.appendChild(stripeJs);
}else{
this.setState({
stripe: window._$stripe_instance
});
}
}
componentWillUnmount() {
this._mounted = false;
}
// render
render() {
const { stripe } = this.state;
return (
<StripeProvider stripe={stripe}>
{this.props.children}
</StripeProvider>
);
}
} if anyone knows a better solution than slapping the instance on window, please advise |
@r3wt I am loading a few external scripts in my project and noticed I was doing the same thing everytime with them. My approach was making a generic script loader class that would keep track of which scripts loaded, instead of storing this state in the window object. It also stores an inflightPromises class ScriptLoader {
constructor() {
this.loadedSDKs = [];
this.inflightRequests = new Map();
this.sdks = {
stripe: {
src: 'https://js.stripe.com/v3/',
id: 'stripe-jssdk',
globalName: 'Stripe',
},
twitter: {
src: 'https://platform.twitter.com/widgets.js',
id: 'twitter-wjs',
globalName: 'twttr',
},
// ...etc
}
this.load = (name) => {
if (this.loadedSDKs.includes(name)) {
return Promise.resolve();
}
const inFlight = this.inflightRequests.get(name);
if (inFlight) {
return inFlight;
}
const inflightPromise = new Promise((res, rej) => {
// Note: This will break if your HTML does not have at least 1 script tag present.
const firstJSTag = document.getElementsByTagName('script')[0];
const sdk = document.createElement('script');
sdk.src = this.sdks[name].src;
sdk.async = true;
sdk.id = this.sdks[name].id;
sdk.onerror = (err) => {
rej(err);
};
sdk.onload = () => {
res();
this.loadedSDKs.push(name);
this.inflightRequests.delete(name);
};
firstJSTag.parentNode.insertBefore(sdk, firstJSTag);
});
this.inflightRequests.set(
name,
inflightPromise,
);
return inflightPromise;
};
this.isLoaded = (name) => {
return this.loadedSDKs.includes(name);
};
}
}
const scriptLoader = new ScriptLoader();
// Singleton, we only want one instance of SDK loader per app.
export default scriptLoader; Where ever you load your Stripe Provider you can use the import { StripeProvider, Elements } from 'react-stripe-elements';
import ScriptLoader from './scriptloader';
// ...
componentDidMount() {
if (ScriptLoader.isLoaded('stripe')) {
this.props.onLoad();
this.setState({
stripe: window.Stripe(this.props.stripePublicKey),
});
} else {
ScriptLoader.load('stripe')
.then(() => {
this.props.onLoad();
this.setState({
stripe: window.Stripe(this.props.stripePublicKey),
});
})
.catch((err) => {
this.props.onError();
});
}
}
// ...
return <StripeProvider stripe={this.state.stripe}>
<Elements>
{this.props.children}
</Elements>
</StripeProvider>; |
@r3wt good point! I think I have mine around my root app, since Stripe claims it helps them better identify fraud (however I encounter your problem with HMR reloads in dev). Can you not just check if @wichopy cool script loader! Why not simplify your componentDidMount() {
this._mounted = true;
ScriptLoader.load('stripe')
.then(() => {
if (this._mounted) {
this.props.onLoad();
this.setState({
stripe: window.Stripe(this.props.stripePublicKey),
});
}
})
.catch((err) => {
this.props.onError();
});
}
}
componentWillUnmount() {
this._mounted = false;
} |
We implemented the async Stripe loading following the README and example. However users found that the Stripe form did not always load properly and required a few page refreshes for it to load.
We discovered an improvement that fixes this issue by checking if the script had already finished loading before the component was created. For example if the Stripe form is on another container from the home page.
The change we made was:
/cc @tonyd256
The text was updated successfully, but these errors were encountered: