forked from stripe/react-stripe-js
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix Elements initialization in React Strict/Concurrent mode
Refs cannot be mutated and used to update state in the same time in rendering phase. As this is side-effect, it can produce various bugs in concurrent mode. In StrictMode Elements doesn't do transition from null to valid stripe instance. As in StrictMode React renders twice, `final` ref becomes `true`, but `ctx` state isn't changed. References: https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects facebook/react#18003 facebook/react#18545
- Loading branch information
Showing
5 changed files
with
132 additions
and
109 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import {FunctionComponent} from 'react'; | ||
import React from 'react'; | ||
import { act } from 'react-dom/test-utils'; | ||
import {mount, ReactWrapper} from 'enzyme'; | ||
import {usePromiseResolver} from './usePromiseResolver'; | ||
import { mockStripe } from '../../test/mocks'; | ||
|
||
const TestComponentInner: FunctionComponent<{value: any}> = () => null | ||
const TestComponent: FunctionComponent<{promiseLike: any}> = ({promiseLike}) => { | ||
const value = usePromiseResolver(promiseLike); | ||
return <TestComponentInner value={value} /> | ||
}; | ||
|
||
const getHookValue = (wrapper: ReactWrapper) => wrapper.find(TestComponentInner).prop('value') | ||
|
||
const createController = (): [Promise<unknown>, (value?: unknown) => Promise<void>, (reason?: any) => Promise<void>] => { | ||
let resolveFn: (value?: unknown) => Promise<void> = () => Promise.resolve() | ||
let rejectFn: (reason?: any) => Promise<void> = () => Promise.resolve() | ||
|
||
const promise = new Promise((resolve, reject) => { | ||
const createVoidPromise = () => promise.then(() => undefined, () => undefined) | ||
|
||
resolveFn = (value) => { | ||
resolve(value) | ||
return createVoidPromise() | ||
} | ||
|
||
rejectFn = (reason) => { | ||
reject(reason) | ||
return createVoidPromise() | ||
} | ||
}) | ||
|
||
return [promise, resolveFn, rejectFn] | ||
} | ||
|
||
describe('usePromiseResolver', () => { | ||
let stripe: ReturnType<typeof mockStripe>; | ||
|
||
beforeEach(() => { | ||
stripe = mockStripe(); | ||
}) | ||
|
||
it('returns value on mount when not promise given', () => { | ||
const wrapper = mount(<TestComponent promiseLike={stripe} />); | ||
expect(wrapper.find(TestComponentInner).prop('value')).toBe(stripe) | ||
}); | ||
|
||
it('returns null on mount when promise given', () => { | ||
const [promise] = createController() | ||
const wrapper = mount(<TestComponent promiseLike={promise} />); | ||
expect(getHookValue(wrapper)).toBeNull() | ||
}); | ||
|
||
it('returns value 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) | ||
}) | ||
}); | ||
|
||
it('returns null 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(() => { | ||
wrapper.update() | ||
expect(getHookValue(wrapper)).toBeNull() | ||
}) | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
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 | ||
}) | ||
|
||
React.useEffect(() => { | ||
if (!isPromise(mayBePromise)) return setResolved(mayBePromise) | ||
|
||
let isMounted = true | ||
|
||
setResolved(null) | ||
mayBePromise.then(resolvedValue => { | ||
if (isMounted) setResolved(resolvedValue) | ||
}, () => undefined) | ||
|
||
return () => { | ||
isMounted = false | ||
} | ||
|
||
}, [mayBePromise]) | ||
|
||
return resolved | ||
}; |