Skip to content

Commit ea7bd2c

Browse files
Prevent recreation of elements instance (#297)
1 parent d5d3f09 commit ea7bd2c

File tree

2 files changed

+110
-8
lines changed

2 files changed

+110
-8
lines changed

‎src/components/Elements.test.tsx‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,5 +334,102 @@ describe('Elements', () => {
334334
expect(mockElements.update).toHaveBeenCalledWith({bar: 'bar'});
335335
expect(mockStripe.elements).toHaveBeenCalledTimes(2);
336336
});
337+
338+
test('creates only one elements instance when updated while resolving Stripe promise', async () => {
339+
let updateResolver: any = () => {};
340+
const updateResult = new Promise<void>((resolve) => {
341+
updateResolver = resolve;
342+
});
343+
344+
let stripePromiseResolve: any = () => {};
345+
const stripePromise = new Promise<any>((resolve) => {
346+
stripePromiseResolve = resolve;
347+
});
348+
349+
// Only resolve stripe once the options have been updated
350+
updateResult.then(() => {
351+
stripePromiseResolve(mockStripePromise);
352+
});
353+
354+
const TestComponent = () => {
355+
const [_, forceRerender] = React.useState(0);
356+
357+
React.useEffect(() => {
358+
setTimeout(() => {
359+
forceRerender((val) => val + 1);
360+
setTimeout(() => {
361+
updateResolver();
362+
});
363+
});
364+
}, []);
365+
366+
return (
367+
<Elements
368+
stripe={stripePromise}
369+
options={{appearance: {theme: 'flat'}}}
370+
/>
371+
);
372+
};
373+
374+
render(<TestComponent />);
375+
376+
await act(async () => await updateResult);
377+
378+
await act(async () => await stripePromise);
379+
380+
expect(mockStripe.elements).toHaveBeenCalledWith({
381+
appearance: {theme: 'flat'},
382+
});
383+
expect(mockStripe.elements).toHaveBeenCalledTimes(1);
384+
});
385+
386+
test('creates only one elements instance when updated while resolving Stripe promise in StrictMode', async () => {
387+
let updateResolver: any = () => {};
388+
const updateResult = new Promise<void>((resolve) => {
389+
updateResolver = resolve;
390+
});
391+
392+
let stripePromiseResolve: any = () => {};
393+
const stripePromise = new Promise<any>((resolve) => {
394+
stripePromiseResolve = resolve;
395+
});
396+
397+
// Only resolve stripe once the options have been updated
398+
updateResult.then(() => {
399+
stripePromiseResolve(mockStripePromise);
400+
});
401+
402+
const TestComponent = () => {
403+
const [_, forceRerender] = React.useState(0);
404+
405+
React.useEffect(() => {
406+
setTimeout(() => {
407+
forceRerender((val) => val + 1);
408+
setTimeout(() => {
409+
updateResolver();
410+
});
411+
});
412+
}, []);
413+
414+
return (
415+
<StrictMode>
416+
<Elements
417+
stripe={stripePromise}
418+
options={{appearance: {theme: 'flat'}}}
419+
/>
420+
</StrictMode>
421+
);
422+
};
423+
424+
render(<TestComponent />);
425+
426+
await act(async () => await updateResult);
427+
await act(async () => await stripePromise);
428+
429+
expect(mockStripe.elements).toHaveBeenCalledWith({
430+
appearance: {theme: 'flat'},
431+
});
432+
expect(mockStripe.elements).toHaveBeenCalledTimes(1);
433+
});
337434
});
338435
});

‎src/components/Elements.tsx‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,25 +125,30 @@ export const Elements: FunctionComponent<PropsWithChildren<ElementsProps>> = (({
125125
React.useEffect(() => {
126126
let isMounted = true;
127127

128+
const safeSetContext = (stripe: stripeJs.Stripe) => {
129+
setContext((ctx) => {
130+
// no-op if we already have a stripe instance (https://github.com/stripe/react-stripe-js/issues/296)
131+
if (ctx.stripe) return ctx;
132+
return {
133+
stripe,
134+
elements: stripe.elements(options),
135+
};
136+
});
137+
};
138+
128139
// For an async stripePromise, store it in context once resolved
129140
if (parsed.tag === 'async' && !ctx.stripe) {
130141
parsed.stripePromise.then((stripe) => {
131142
if (stripe && isMounted) {
132143
// Only update Elements context if the component is still mounted
133144
// and stripe is not null. We allow stripe to be null to make
134145
// handling SSR easier.
135-
setContext({
136-
stripe,
137-
elements: stripe.elements(options),
138-
});
146+
safeSetContext(stripe);
139147
}
140148
});
141149
} else if (parsed.tag === 'sync' && !ctx.stripe) {
142150
// Or, handle a sync stripe instance going from null -> populated
143-
setContext({
144-
stripe: parsed.stripe,
145-
elements: parsed.stripe.elements(options),
146-
});
151+
safeSetContext(parsed.stripe);
147152
}
148153

149154
return () => {

0 commit comments

Comments
 (0)