Merge pull request #824 from streamich/useUnmountPromise

feat: 🎸 add useUnmountPromise hook
This commit is contained in:
Vadim Dalecky 2019-12-07 15:59:53 -08:00 committed by GitHub
commit 6aa9984a3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 144 additions and 1 deletions

View File

@ -111,7 +111,7 @@
- [`useMountedState`](./docs/useMountedState.md) — track if component is mounted.
- [`usePromise`](./docs/usePromise.md) — resolves promise only while component is mounted.
- [`useLogger`](./docs/useLogger.md) — logs in console as component goes through life-cycles.
- [`useMount`](./docs/useMount.md) — calls `mount` callbacks.
- [`useMount`](./docs/useMount.md) and [`useUnmountPromise`](./docs/useUnmountPromise.md) — tracks if component is mounted.
- [`useUnmount`](./docs/useUnmount.md) — calls `unmount` callbacks.
- [`useUpdateEffect`](./docs/useUpdateEffect.md) — run an `effect` only on updates.
- [`useIsomorphicLayoutEffect`](./docs/useIsomorphicLayoutEffect.md) — `useLayoutEffect` that does not show warning when server-side rendering.

30
docs/useUnmountPromise.md Normal file
View File

@ -0,0 +1,30 @@
# `useUnmountPromise`
A life-cycle hook that provides a higher order promise that does not resolve if component un-mounts.
## Usage
```ts
import useUnmountPromise from 'react-use/lib/useUnmountPromise';
const Demo = () => {
const mounted = useUnmountPromise();
useEffect(async () => {
await mounted(someFunction()); // Will not resolve if component un-mounts.
});
};
```
## Reference
```ts
const mounted = useUnmountPromise();
mounted(promise);
mounted(promise, onError);
```
- `onError` — if promise rejects after the component is unmounted, `onError`
callback is called with the error.

View File

@ -85,6 +85,7 @@ export { default as useTitle } from './useTitle';
export { default as useToggle } from './useToggle';
export { default as useTween } from './useTween';
export { default as useUnmount } from './useUnmount';
export { default as useUnmountPromise } from './useUnmountPromise';
export { default as useUpdate } from './useUpdate';
export { default as useUpdateEffect } from './useUpdateEffect';
export { default as useUpsert } from './useUpsert';

33
src/useUnmountPromise.ts Normal file
View File

@ -0,0 +1,33 @@
import { useMemo, useRef, useEffect } from 'react';
export type Race = <P extends Promise<any>, E = any>(promise: P, onError?: (error: E) => void) => P;
const useUnmountPromise = (): Race => {
const refUnmounted = useRef(false);
useEffect(() => () => {
refUnmounted.current = true;
});
const wrapper = useMemo(() => {
const race = <P extends Promise<any>, E>(promise: P, onError?: (error: E) => void) => {
const newPromise: P = new Promise((resolve, reject) => {
promise.then(
result => {
if (!refUnmounted.current) resolve(result);
},
error => {
if (!refUnmounted.current) reject(error);
else if (onError) onError(error);
else console.error('useUnmountPromise', error);
}
);
}) as P;
return newPromise;
};
return race;
}, []);
return wrapper;
};
export default useUnmountPromise;

View File

@ -0,0 +1,79 @@
import { renderHook } from '@testing-library/react-hooks';
import useUnmountPromise from '../src/useUnmountPromise';
describe('useUnmountPromise', () => {
it('should be defined', () => {
expect(useUnmountPromise).toBeDefined();
});
it('should return a function', () => {
const hook = renderHook(() => useUnmountPromise());
expect(typeof hook.result.current).toBe('function');
});
it('when component is mounted function should resolve with wrapped promises', async () => {
const hook = renderHook(() => useUnmountPromise());
const mounted = hook.result.current;
const res = await mounted(new Promise(r => setTimeout(() => r(25), 10)));
expect(res).toBe(25);
});
it('when component is unmounted promise never resolves', async () => {
const hook = renderHook(() => useUnmountPromise());
const mounted = hook.result.current;
const promise = mounted(new Promise(r => setTimeout(() => r(25), 10)));
hook.unmount();
const res = await Promise.race([promise, new Promise(r => setTimeout(() => r('UNMOUNTED'), 20))]);
expect(res).toBe('UNMOUNTED');
});
it('when component is mounted function should resolve with wrapped promises - 2', async () => {
const hook = renderHook(() => useUnmountPromise());
const mounted = hook.result.current;
const promise = mounted(new Promise(r => setTimeout(() => r(25), 10)));
// hook.unmount();
const res = await Promise.race([promise, new Promise(r => setTimeout(() => r('UNMOUNTED'), 20))]);
expect(res).toBe(25);
});
describe('when promise throws', () => {
describe('when component is mounted', () => {
it('onError callback is not called', async () => {
const hook = renderHook(() => useUnmountPromise());
const mounted = hook.result.current;
const onError = jest.fn();
try {
await mounted(new Promise((r, reject) => setTimeout(() => reject(r), 10)), onError);
} catch {}
expect(onError).toHaveBeenCalledTimes(0);
});
});
describe('when component is un-mounted', () => {
it('onError callback is called', async () => {
const hook = renderHook(() => useUnmountPromise());
const mounted = hook.result.current;
const onError = jest.fn();
const promise = mounted(new Promise((r, reject) => setTimeout(() => reject(r), 10)), onError);
hook.unmount();
await Promise.race([promise, new Promise(r => setTimeout(r, 20))]);
expect(onError).toHaveBeenCalledTimes(1);
expect(typeof onError.mock.calls[0][0]).toBe('function');
});
});
});
});