Skip to content

Commit

Permalink
Merge pull request streamich#824 from streamich/useUnmountPromise
Browse files Browse the repository at this point in the history
feat: 🎸 add useUnmountPromise hook
  • Loading branch information
streamich authored Dec 7, 2019
2 parents 36a34e8 + 28ef34b commit 6aa9984
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 30 additions & 0 deletions docs/useUnmountPromise.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
33 changes: 33 additions & 0 deletions src/useUnmountPromise.ts
Original file line number Diff line number Diff line change
@@ -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;
79 changes: 79 additions & 0 deletions tests/useUnmountPromise.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
});

0 comments on commit 6aa9984

Please sign in to comment.