Skip to content

Commit

Permalink
Improve Elements component readability
Browse files Browse the repository at this point in the history
- Initialize context on mount with given stripe value
- Make `usePromiseResolver` more generic purpose
- Get rid of props destructuring
  • Loading branch information
khmm12 committed May 20, 2020
1 parent cdeb74a commit 73bfa3d
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 44 deletions.
16 changes: 16 additions & 0 deletions src/components/Elements.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ describe('Elements', () => {
expect(wrapper.find(TestComponent).prop('stripe')).toBe(stripe);
});

it('provides given stripe instance on mount', () => {
expect(() => {
mount(
<Elements stripe={stripe}>
<ElementsConsumer>
{(ctx) => {
if (ctx.stripe === null)
throw new TypeError('Stripe instance is null');
return null;
}}
</ElementsConsumer>
</Elements>
);
}).not.toThrow(/stripe instance is null/i);
});

it('allows a transition from null to a valid Stripe object', () => {
const wrapper = mount(
<Elements stripe={null}>
Expand Down
43 changes: 23 additions & 20 deletions src/components/Elements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,19 @@ interface PrivateElementsProps {
*
* @docs https://stripe.com/docs/stripe-js/react#elements-provider
*/
export const Elements: FunctionComponent<ElementsProps> = ({
stripe: rawStripeProp,
options: optionsProp,
children,
}: PrivateElementsProps) => {
const [inputs, setInputs] = React.useState({ rawStripe: rawStripeProp, options: optionsProp })
const { rawStripe, options } = inputs
export const Elements: FunctionComponent<ElementsProps> = (props: PrivateElementsProps) => {
const { children } = props

if (props.stripe === undefined) throw new Error(INVALID_STRIPE_ERROR);

const [inputs, setInputs] = React.useState({ rawStripe: props.stripe, options: props.options })
React.useEffect(() => {
const hasRawStripeChanged = rawStripe !== rawStripeProp
const hasOptionsChanged = !isEqual(options, optionsProp)
const { rawStripe, options } = inputs
const { stripe: nextRawStripe, options: nextOptions } = props

const canUpdate = rawStripe === null
const hasRawStripeChanged = rawStripe !== nextRawStripe
const hasOptionsChanged = !isEqual(options, nextOptions)

if (hasRawStripeChanged && !canUpdate) {
console.warn(
Expand All @@ -109,27 +111,28 @@ export const Elements: FunctionComponent<ElementsProps> = ({
);
}

if (hasRawStripeChanged && canUpdate) setInputs({ rawStripe: rawStripeProp, options: optionsProp })
}, [rawStripe, options, rawStripeProp, optionsProp])
const nextInputs = { rawStripe: nextRawStripe, options: nextOptions }
if (hasRawStripeChanged && canUpdate) setInputs(nextInputs)
}, [inputs, props])

const maybeStripe = usePromiseResolver(rawStripe)
const stripe = validateStripe(maybeStripe)
const [ctx, setContext] = React.useState<ElementsContextValue>(() => createElementsContext(null));
const [maybeStripe = null] = usePromiseResolver(inputs.rawStripe)
const resolvedStripe = validateStripe(maybeStripe)
const [ctx, setContext] = React.useState(() => createElementsContext(resolvedStripe, inputs.options));

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

React.useEffect(() => {
const anyStripe: any = stripe;
const anyStripe: any = ctx.stripe;

if (!anyStripe || !anyStripe._registerWrapper) {
return;
}

anyStripe._registerWrapper({name: 'react-stripe-js', version: _VERSION});
}, [stripe]);

React.useEffect(() => {
if (stripe) setContext(createElementsContext(stripe, options))
}, [stripe, options])
}, [ctx.stripe]);

return (
<ElementsContext.Provider value={ctx}>{children}</ElementsContext.Provider>
Expand Down
19 changes: 10 additions & 9 deletions src/utils/usePromiseResolver.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,34 +41,35 @@ describe('usePromiseResolver', () => {
stripe = mockStripe();
})

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

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

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

return Promise.resolve(act(() => resolve(stripe))).then(() => {
wrapper.update()
expect(getHookValue(wrapper)).toBe(stripe)
expect(getHookValue(wrapper)).toEqual([stripe, undefined, 'resolved'])
})
});

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

return Promise.resolve(act(() => reject(new Error('Something went wrong')))).then(() => {
const error = new Error('Something went wrong')
return Promise.resolve(act(() => reject(error))).then(() => {
wrapper.update()
expect(getHookValue(wrapper)).toBeNull()
expect(getHookValue(wrapper)).toEqual([undefined, error, 'rejected'])
})
});
});
55 changes: 40 additions & 15 deletions src/utils/usePromiseResolver.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,51 @@
import React from 'react';
import {isPromise} from '../utils/guards';

export const usePromiseResolver = <T>(mayBePromise: T | PromiseLike<T>): T | null => {
const [resolved, setResolved] = React.useState(() => {
return isPromise(mayBePromise) ? null : mayBePromise
})
type PromisePending = [undefined, undefined, 'pending'];
type PromiseResolved<T> = [T, undefined, 'resolved'];
type PromiseRejected = [undefined, any, 'rejected'];
type PromiseState<T> = PromisePending | PromiseResolved<T> | PromiseRejected;

const createPending = (): PromisePending => [undefined, undefined, 'pending'];

const createResolved = <T>(value: T): PromiseResolved<T> => [
value,
undefined,
'resolved',
];

const createRejected = (reason: any): PromiseRejected => [
undefined,
reason,
'rejected',
];

export const usePromiseResolver = <T>(
mayBePromise: T | PromiseLike<T>
): PromiseState<T> => {
const [state, setState] = React.useState<PromiseState<T>>(() =>
isPromise(mayBePromise) ? createPending() : createResolved(mayBePromise)
);

React.useEffect(() => {
if (!isPromise(mayBePromise)) return setResolved(mayBePromise)
if (!isPromise(mayBePromise)) return setState(createResolved(mayBePromise));

let isMounted = true
let isMounted = true;

setResolved(null)
mayBePromise.then(resolvedValue => {
if (isMounted) setResolved(resolvedValue)
}, () => undefined)
setState(createPending());
mayBePromise
.then(
(resolved) => createResolved(resolved),
(error) => createRejected(error)
)
.then((nextState) => {
if (isMounted) setState(nextState);
});

return () => {
isMounted = false
}

}, [mayBePromise])
isMounted = false;
};
}, [mayBePromise]);

return resolved
return state;
};

0 comments on commit 73bfa3d

Please sign in to comment.