From 5bcac96a2602c68cb9a9c670c6b8ea6bc6e9b138 Mon Sep 17 00:00:00 2001 From: jpwallace22 Date: Sun, 11 Dec 2022 20:40:02 -0800 Subject: [PATCH 1/6] Run new-hook and write usePromise --- src/index.ts | 2 ++ src/usePromise/__docs__/example.stories.tsx | 6 +++++ src/usePromise/__docs__/story.mdx | 27 +++++++++++++++++++++ src/usePromise/__tests__/dom.ts | 13 ++++++++++ src/usePromise/__tests__/ssr.ts | 13 ++++++++++ src/usePromise/usePromise.ts | 23 ++++++++++++++++++ 6 files changed, 84 insertions(+) create mode 100644 src/usePromise/__docs__/example.stories.tsx create mode 100644 src/usePromise/__docs__/story.mdx create mode 100644 src/usePromise/__tests__/dom.ts create mode 100644 src/usePromise/__tests__/ssr.ts create mode 100644 src/usePromise/usePromise.ts diff --git a/src/index.ts b/src/index.ts index 5b26ac9d..86c1b170 100644 --- a/src/index.ts +++ b/src/index.ts @@ -116,3 +116,5 @@ export { resolveHookState } from './util/resolveHookState'; export * from './types'; export { useDeepCompareMemo } from './useDeepCompareMemo/useDeepCompareMemo'; + +export { usePromise } from './usePromise/usePromise'; diff --git a/src/usePromise/__docs__/example.stories.tsx b/src/usePromise/__docs__/example.stories.tsx new file mode 100644 index 00000000..c25065cb --- /dev/null +++ b/src/usePromise/__docs__/example.stories.tsx @@ -0,0 +1,6 @@ +import * as React from 'react'; +// import { usePromise } from '../..'; + +export const Example: React.FC = () => { + return null; +}; diff --git a/src/usePromise/__docs__/story.mdx b/src/usePromise/__docs__/story.mdx new file mode 100644 index 00000000..732b4dfe --- /dev/null +++ b/src/usePromise/__docs__/story.mdx @@ -0,0 +1,27 @@ +import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; +import { Example } from './example.stories'; +import { ImportPath } from '../../storybookUtil/ImportPath'; + + + +# usePromise + +#### Example + + + + + +## Reference + +```ts + +``` + +#### Importing + + + +#### Arguments + +#### Return diff --git a/src/usePromise/__tests__/dom.ts b/src/usePromise/__tests__/dom.ts new file mode 100644 index 00000000..2024f47c --- /dev/null +++ b/src/usePromise/__tests__/dom.ts @@ -0,0 +1,13 @@ +import { renderHook } from '@testing-library/react-hooks/dom'; +import { usePromise } from '../..'; + +describe('usePromise', () => { + it('should be defined', () => { + expect(usePromise).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => usePromise()); + expect(result.error).toBeUndefined(); + }); +}); diff --git a/src/usePromise/__tests__/ssr.ts b/src/usePromise/__tests__/ssr.ts new file mode 100644 index 00000000..37cc187b --- /dev/null +++ b/src/usePromise/__tests__/ssr.ts @@ -0,0 +1,13 @@ +import { renderHook } from '@testing-library/react-hooks/server'; +import { usePromise } from '../..'; + +describe('usePromise', () => { + it('should be defined', () => { + expect(usePromise).toBeDefined(); + }); + + it('should render', () => { + const { result } = renderHook(() => usePromise()); + expect(result.error).toBeUndefined(); + }); +}); diff --git a/src/usePromise/usePromise.ts b/src/usePromise/usePromise.ts new file mode 100644 index 00000000..039392cf --- /dev/null +++ b/src/usePromise/usePromise.ts @@ -0,0 +1,23 @@ +import { useCallback } from 'react'; +import { useIsMounted } from '../useIsMounted/useIsMounted'; + +export type UsePromise = () => (promise: Promise) => Promise; + +export const usePromise: UsePromise = () => { + const isMounted = useIsMounted(); + return useCallback( + (promise: Promise) => + new Promise((resolve, reject) => { + const onResolve = (resolution: T) => { + return isMounted() && resolve(resolution); + }; + const onReject = (rejection: T) => { + return isMounted() && reject(rejection); + }; + + // eslint-disable-next-line promise/catch-or-return + promise.then(onResolve, onReject); + }), + [isMounted] + ); +}; From 294c0e72a7a85ff182b0ca447d5c5d8153fe2fde Mon Sep 17 00:00:00 2001 From: jpwallace22 Date: Sun, 11 Dec 2022 21:16:25 -0800 Subject: [PATCH 2/6] add example story --- src/usePromise/__docs__/example.stories.tsx | 29 +++++++++++++++++++-- src/usePromise/usePromise.ts | 11 ++++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/usePromise/__docs__/example.stories.tsx b/src/usePromise/__docs__/example.stories.tsx index c25065cb..5b15692e 100644 --- a/src/usePromise/__docs__/example.stories.tsx +++ b/src/usePromise/__docs__/example.stories.tsx @@ -1,6 +1,31 @@ import * as React from 'react'; -// import { usePromise } from '../..'; +import { useState, useEffect } from 'react'; +import { usePromise } from '../..'; export const Example: React.FC = () => { - return null; + const mounted = usePromise(); + const [value, setValue] = useState(); + + const myPromise = new Promise((resolve) => { + setTimeout(() => { + resolve('foo'); + }, 300); + }); + + useEffect(() => { + (async () => { + const res = await mounted(myPromise); + + if (typeof res === 'string') { + // This line will not execute if this component gets unmounted. + setValue(res); + } + })(); + }); + + return ( +
+ +
+ ); }; diff --git a/src/usePromise/usePromise.ts b/src/usePromise/usePromise.ts index 039392cf..69b81995 100644 --- a/src/usePromise/usePromise.ts +++ b/src/usePromise/usePromise.ts @@ -1,19 +1,18 @@ import { useCallback } from 'react'; import { useIsMounted } from '../useIsMounted/useIsMounted'; +import { noop } from '../util/const'; export type UsePromise = () => (promise: Promise) => Promise; export const usePromise: UsePromise = () => { const isMounted = useIsMounted(); + return useCallback( (promise: Promise) => new Promise((resolve, reject) => { - const onResolve = (resolution: T) => { - return isMounted() && resolve(resolution); - }; - const onReject = (rejection: T) => { - return isMounted() && reject(rejection); - }; + const onResolve = (resolution: T) => (isMounted() ? resolve(resolution) : noop); + + const onReject = (rejection: unknown) => (isMounted() ? reject(rejection) : noop); // eslint-disable-next-line promise/catch-or-return promise.then(onResolve, onReject); From 21199f9795081a5f7ac664bf4cbe7220bad7e3b4 Mon Sep 17 00:00:00 2001 From: jpwallace22 Date: Mon, 12 Dec 2022 10:13:30 -0800 Subject: [PATCH 3/6] create dom tests. Error with jest timer length --- src/usePromise/__docs__/story.mdx | 2 ++ src/usePromise/__tests__/dom.ts | 13 +++++++++++++ src/usePromise/usePromise.ts | 5 ++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/usePromise/__docs__/story.mdx b/src/usePromise/__docs__/story.mdx index 732b4dfe..eb9dde5c 100644 --- a/src/usePromise/__docs__/story.mdx +++ b/src/usePromise/__docs__/story.mdx @@ -6,6 +6,8 @@ import { ImportPath } from '../../storybookUtil/ImportPath'; # usePromise +React Lifecycle hook that returns a helper function for wrapping promises. Promises wrapped with this function will resolve only when component is mounted. + #### Example diff --git a/src/usePromise/__tests__/dom.ts b/src/usePromise/__tests__/dom.ts index 2024f47c..8005d03a 100644 --- a/src/usePromise/__tests__/dom.ts +++ b/src/usePromise/__tests__/dom.ts @@ -2,6 +2,10 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { usePromise } from '../..'; describe('usePromise', () => { + const promise = new Promise((resolve) => { + resolve(1); + }); + it('should be defined', () => { expect(usePromise).toBeDefined(); }); @@ -10,4 +14,13 @@ describe('usePromise', () => { const { result } = renderHook(() => usePromise()); expect(result.error).toBeUndefined(); }); + + it('should not return if unmounted', async () => { + const { result, unmount } = renderHook(() => usePromise()); + unmount(); + const res = await result.current(promise); + + expect(result.error).toBeUndefined(); + expect(res).toBeUndefined(); + }); }); diff --git a/src/usePromise/usePromise.ts b/src/usePromise/usePromise.ts index 69b81995..5ac8c8c5 100644 --- a/src/usePromise/usePromise.ts +++ b/src/usePromise/usePromise.ts @@ -1,6 +1,5 @@ import { useCallback } from 'react'; import { useIsMounted } from '../useIsMounted/useIsMounted'; -import { noop } from '../util/const'; export type UsePromise = () => (promise: Promise) => Promise; @@ -10,9 +9,9 @@ export const usePromise: UsePromise = () => { return useCallback( (promise: Promise) => new Promise((resolve, reject) => { - const onResolve = (resolution: T) => (isMounted() ? resolve(resolution) : noop); + const onResolve = (resolution: T) => isMounted() && resolve(resolution); - const onReject = (rejection: unknown) => (isMounted() ? reject(rejection) : noop); + const onReject = (rejection: unknown) => isMounted() && reject(rejection); // eslint-disable-next-line promise/catch-or-return promise.then(onResolve, onReject); From bec4251e225811f6a9062e46ad11f7fd6442777c Mon Sep 17 00:00:00 2001 From: jpwallace22 Date: Mon, 12 Dec 2022 11:04:55 -0800 Subject: [PATCH 4/6] finish writing tests (that I can figure out) --- src/usePromise/__tests__/dom.ts | 39 +++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/usePromise/__tests__/dom.ts b/src/usePromise/__tests__/dom.ts index 8005d03a..6e1df27c 100644 --- a/src/usePromise/__tests__/dom.ts +++ b/src/usePromise/__tests__/dom.ts @@ -1,9 +1,20 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { usePromise } from '../..'; +jest.useFakeTimers(); + describe('usePromise', () => { - const promise = new Promise((resolve) => { + const resolves = new Promise((resolve, reject) => { resolve(1); + reject(Error); + }); + + const rejects = new Promise((resolve, reject) => { + // eslint-disable-next-line no-constant-condition + if (1 < 0) { + resolve(1); + } + reject(Error); }); it('should be defined', () => { @@ -15,12 +26,26 @@ describe('usePromise', () => { expect(result.error).toBeUndefined(); }); - it('should not return if unmounted', async () => { - const { result, unmount } = renderHook(() => usePromise()); - unmount(); - const res = await result.current(promise); + it('should return resolved value', async () => { + const { result } = renderHook(() => usePromise()); - expect(result.error).toBeUndefined(); - expect(res).toBeUndefined(); + await expect(result.current(resolves)).resolves.toBe(1); + }); + + it('should return rejection value', async () => { + const { result } = renderHook(() => usePromise()); + + expect.assertions(1); + await expect(result.current(rejects)).rejects.toBe(Error); }); + + // it('should not return if unmounted', async () => { + // const { result, unmount } = renderHook(() => usePromise()); + // unmount(); + + // expect(result.error).toBeUndefined(); + // await expect(result.current(resolves)).resolves.toBeUndefined(); + // }); }); + +jest.clearAllTimers(); From fe03b58b3fe250145e623be0c765b707d6c17a58 Mon Sep 17 00:00:00 2001 From: jpwallace22 Date: Mon, 12 Dec 2022 16:58:22 -0800 Subject: [PATCH 5/6] Write as much tests that I can --- src/usePromise/__tests__/dom.ts | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/src/usePromise/__tests__/dom.ts b/src/usePromise/__tests__/dom.ts index 6e1df27c..807cff3b 100644 --- a/src/usePromise/__tests__/dom.ts +++ b/src/usePromise/__tests__/dom.ts @@ -1,21 +1,10 @@ import { renderHook } from '@testing-library/react-hooks/dom'; import { usePromise } from '../..'; -jest.useFakeTimers(); - describe('usePromise', () => { - const resolves = new Promise((resolve, reject) => { - resolve(1); - reject(Error); - }); - - const rejects = new Promise((resolve, reject) => { - // eslint-disable-next-line no-constant-condition - if (1 < 0) { - resolve(1); - } - reject(Error); - }); + const thenFn = jest.fn(); + const resolves = Promise.resolve(thenFn); + const rejects = Promise.reject(Error); it('should be defined', () => { expect(usePromise).toBeDefined(); @@ -29,23 +18,19 @@ describe('usePromise', () => { it('should return resolved value', async () => { const { result } = renderHook(() => usePromise()); - await expect(result.current(resolves)).resolves.toBe(1); + await expect(result.current(resolves)).resolves.toBe(thenFn); }); it('should return rejection value', async () => { const { result } = renderHook(() => usePromise()); - expect.assertions(1); await expect(result.current(rejects)).rejects.toBe(Error); }); - // it('should not return if unmounted', async () => { + // Test fails due to being timed out. But it DOES prevent the promise from being called. + // test('should not return if unmounted', async () => { // const { result, unmount } = renderHook(() => usePromise()); // unmount(); - - // expect(result.error).toBeUndefined(); // await expect(result.current(resolves)).resolves.toBeUndefined(); // }); }); - -jest.clearAllTimers(); From 5823e5780d93443a7422fc91f8755f72287a66a8 Mon Sep 17 00:00:00 2001 From: jpwallace22 Date: Mon, 12 Dec 2022 19:52:42 -0800 Subject: [PATCH 6/6] complete docs and add to README --- README.md | 2 ++ src/usePromise/__docs__/story.mdx | 11 +++++++---- src/usePromise/__tests__/dom.ts | 7 ------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8aefbb86..d22d2d41 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,8 @@ Coming from `react-use`? Check out our — Like `useLayoutEffect` but falls back to `useEffect` during SSR. - [**`useMountEffect`**](https://react-hookz.github.io/web/?path=/docs/lifecycle-usemounteffect--example) — Run an effect only when a component mounts. + - [**`usePromise`**](https://react-hookz.github.io/web/?path=/docs/lifecycle-usepromise--example) + — Resolves promise only while component is mounted. - [**`useRafEffect`**](https://react-hookz.github.io/web/?path=/docs/lifecycle-useRafEffect--example) — Like `useEffect`, but the effect is only run within an animation frame. - [**`useRerender`**](https://react-hookz.github.io/web/?path=/docs/lifecycle-usererender--example) diff --git a/src/usePromise/__docs__/story.mdx b/src/usePromise/__docs__/story.mdx index eb9dde5c..4bc1d3f8 100644 --- a/src/usePromise/__docs__/story.mdx +++ b/src/usePromise/__docs__/story.mdx @@ -6,7 +6,7 @@ import { ImportPath } from '../../storybookUtil/ImportPath'; # usePromise -React Lifecycle hook that returns a helper function for wrapping promises. Promises wrapped with this function will resolve only when component is mounted. +React lifecycle hook that returns a helper function for wrapping promises. Promises wrapped with this function will resolve only if component is mounted. #### Example @@ -17,13 +17,16 @@ React Lifecycle hook that returns a helper function for wrapping promises. Promi ## Reference ```ts - +usePromise = + () => + (promise: Promise) => + Promise; ``` #### Importing -#### Arguments - #### Return + +_`(promise: Promise) => Promise`_ — Function wrapper that for a promise. diff --git a/src/usePromise/__tests__/dom.ts b/src/usePromise/__tests__/dom.ts index 807cff3b..8ec74151 100644 --- a/src/usePromise/__tests__/dom.ts +++ b/src/usePromise/__tests__/dom.ts @@ -26,11 +26,4 @@ describe('usePromise', () => { await expect(result.current(rejects)).rejects.toBe(Error); }); - - // Test fails due to being timed out. But it DOES prevent the promise from being called. - // test('should not return if unmounted', async () => { - // const { result, unmount } = renderHook(() => usePromise()); - // unmount(); - // await expect(result.current(resolves)).resolves.toBeUndefined(); - // }); });