Skip to content

Commit 58fad83

Browse files
committed
Improve Elements component readability
- Initialize context on mount with given stripe value - Make `usePromiseResolver` more generic purpose - Get rid of props destructuring
1 parent cdeb74a commit 58fad83

4 files changed

Lines changed: 87 additions & 44 deletions

File tree

src/components/Elements.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,22 @@ describe('Elements', () => {
7272
expect(wrapper.find(TestComponent).prop('stripe')).toBe(stripe);
7373
});
7474

75+
it('provides given stripe instance on mount', () => {
76+
expect(() => {
77+
mount(
78+
<Elements stripe={stripe}>
79+
<ElementsConsumer>
80+
{(ctx) => {
81+
if (ctx.stripe === null)
82+
throw new TypeError('Stripe instance is null');
83+
return null;
84+
}}
85+
</ElementsConsumer>
86+
</Elements>
87+
);
88+
}).not.toThrow(/stripe instance is null/i);
89+
});
90+
7591
it('allows a transition from null to a valid Stripe object', () => {
7692
const wrapper = mount(
7793
<Elements stripe={null}>

src/components/Elements.tsx

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,17 @@ interface PrivateElementsProps {
8585
*
8686
* @docs https://stripe.com/docs/stripe-js/react#elements-provider
8787
*/
88-
export const Elements: FunctionComponent<ElementsProps> = ({
89-
stripe: rawStripeProp,
90-
options: optionsProp,
91-
children,
92-
}: PrivateElementsProps) => {
93-
const [inputs, setInputs] = React.useState({ rawStripe: rawStripeProp, options: optionsProp })
94-
const { rawStripe, options } = inputs
88+
export const Elements: FunctionComponent<ElementsProps> = (props: PrivateElementsProps) => {
89+
const { children } = props
90+
91+
if (props.stripe === undefined) throw new Error(INVALID_STRIPE_ERROR);
92+
93+
const [inputs, setInputs] = React.useState({ rawStripe: props.stripe, options: props.options })
9594
React.useEffect(() => {
96-
const hasRawStripeChanged = rawStripe !== rawStripeProp
97-
const hasOptionsChanged = !isEqual(options, optionsProp)
95+
const { stripe: previousRawStripe, options: previousOptions } = props
96+
const { rawStripe, options } = inputs
97+
const hasRawStripeChanged = rawStripe !== previousRawStripe
98+
const hasOptionsChanged = !isEqual(options, previousOptions)
9899
const canUpdate = rawStripe === null
99100

100101
if (hasRawStripeChanged && !canUpdate) {
@@ -109,27 +110,27 @@ export const Elements: FunctionComponent<ElementsProps> = ({
109110
);
110111
}
111112

112-
if (hasRawStripeChanged && canUpdate) setInputs({ rawStripe: rawStripeProp, options: optionsProp })
113-
}, [rawStripe, options, rawStripeProp, optionsProp])
113+
if (hasRawStripeChanged && canUpdate) setInputs({ rawStripe: previousRawStripe, options: previousOptions })
114+
}, [inputs, props])
114115

115-
const maybeStripe = usePromiseResolver(rawStripe)
116-
const stripe = validateStripe(maybeStripe)
117-
const [ctx, setContext] = React.useState<ElementsContextValue>(() => createElementsContext(null));
116+
const [maybeStripe = null] = usePromiseResolver(inputs.rawStripe)
117+
const resolvedStripe = validateStripe(maybeStripe)
118+
const [ctx, setContext] = React.useState(() => createElementsContext(resolvedStripe, inputs.options));
118119

120+
const shouldInitialize = resolvedStripe !== null && ctx.stripe === null
121+
React.useEffect(() => {
122+
if (shouldInitialize) setContext(createElementsContext(resolvedStripe, inputs.options))
123+
}, [shouldInitialize, resolvedStripe, inputs.options])
119124

120125
React.useEffect(() => {
121-
const anyStripe: any = stripe;
126+
const anyStripe: any = ctx.stripe;
122127

123128
if (!anyStripe || !anyStripe._registerWrapper) {
124129
return;
125130
}
126131

127132
anyStripe._registerWrapper({name: 'react-stripe-js', version: _VERSION});
128-
}, [stripe]);
129-
130-
React.useEffect(() => {
131-
if (stripe) setContext(createElementsContext(stripe, options))
132-
}, [stripe, options])
133+
}, [ctx.stripe]);
133134

134135
return (
135136
<ElementsContext.Provider value={ctx}>{children}</ElementsContext.Provider>

src/utils/usePromiseResolver.test.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,34 +41,35 @@ describe('usePromiseResolver', () => {
4141
stripe = mockStripe();
4242
})
4343

44-
it('returns value on mount when not promise given', () => {
44+
it('returns resolved on mount when not promise given', () => {
4545
const wrapper = mount(<TestComponent promiseLike={stripe} />);
46-
expect(wrapper.find(TestComponentInner).prop('value')).toBe(stripe)
46+
expect(getHookValue(wrapper)).toEqual([stripe, undefined, 'resolved'])
4747
});
4848

49-
it('returns null on mount when promise given', () => {
49+
it('returns pending on mount when promise given', () => {
5050
const [promise] = createController()
5151
const wrapper = mount(<TestComponent promiseLike={promise} />);
52-
expect(getHookValue(wrapper)).toBeNull()
52+
expect(getHookValue(wrapper)).toEqual([undefined, undefined, 'pending'])
5353
});
5454

55-
it('returns value when given promise resolved', () => {
55+
it('returns resolved when given promise resolved', () => {
5656
const [promise, resolve] = createController()
5757
const wrapper = mount(<TestComponent promiseLike={promise} />);
5858

5959
return Promise.resolve(act(() => resolve(stripe))).then(() => {
6060
wrapper.update()
61-
expect(getHookValue(wrapper)).toBe(stripe)
61+
expect(getHookValue(wrapper)).toEqual([stripe, undefined, 'resolved'])
6262
})
6363
});
6464

65-
it('returns null when given promise rejected', () => {
65+
it('returns rejected when given promise rejected', () => {
6666
const [promise,, reject] = createController()
6767
const wrapper = mount(<TestComponent promiseLike={promise} />);
6868

69-
return Promise.resolve(act(() => reject(new Error('Something went wrong')))).then(() => {
69+
const error = new Error('Something went wrong')
70+
return Promise.resolve(act(() => reject(error))).then(() => {
7071
wrapper.update()
71-
expect(getHookValue(wrapper)).toBeNull()
72+
expect(getHookValue(wrapper)).toEqual([undefined, error, 'rejected'])
7273
})
7374
});
7475
});

src/utils/usePromiseResolver.ts

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,51 @@
11
import React from 'react';
22
import {isPromise} from '../utils/guards';
33

4-
export const usePromiseResolver = <T>(mayBePromise: T | PromiseLike<T>): T | null => {
5-
const [resolved, setResolved] = React.useState(() => {
6-
return isPromise(mayBePromise) ? null : mayBePromise
7-
})
4+
type PromisePending = [undefined, undefined, 'pending'];
5+
type PromiseResolved<T> = [T, undefined, 'resolved'];
6+
type PromiseRejected = [undefined, any, 'rejected'];
7+
type PromiseState<T> = PromisePending | PromiseResolved<T> | PromiseRejected;
8+
9+
const createPending = (): PromisePending => [undefined, undefined, 'pending'];
10+
11+
const createResolved = <T>(value: T): PromiseResolved<T> => [
12+
value,
13+
undefined,
14+
'resolved',
15+
];
16+
17+
const createRejected = (reason: any): PromiseRejected => [
18+
undefined,
19+
reason,
20+
'rejected',
21+
];
22+
23+
export const usePromiseResolver = <T>(
24+
mayBePromise: T | PromiseLike<T>
25+
): PromiseState<T> => {
26+
const [state, setState] = React.useState<PromiseState<T>>(() =>
27+
isPromise(mayBePromise) ? createPending() : createResolved(mayBePromise)
28+
);
829

930
React.useEffect(() => {
10-
if (!isPromise(mayBePromise)) return setResolved(mayBePromise)
31+
if (!isPromise(mayBePromise)) return setState(createResolved(mayBePromise));
1132

12-
let isMounted = true
33+
let isMounted = true;
1334

14-
setResolved(null)
15-
mayBePromise.then(resolvedValue => {
16-
if (isMounted) setResolved(resolvedValue)
17-
}, () => undefined)
35+
setState(createPending());
36+
mayBePromise
37+
.then(
38+
(resolved) => createResolved(resolved),
39+
(error) => createRejected(error)
40+
)
41+
.then((nextState) => {
42+
if (isMounted) setState(nextState);
43+
});
1844

1945
return () => {
20-
isMounted = false
21-
}
22-
23-
}, [mayBePromise])
46+
isMounted = false;
47+
};
48+
}, [mayBePromise]);
2449

25-
return resolved
50+
return state;
2651
};

0 commit comments

Comments
 (0)