From cc3f0f9ecb8d6a229a36caa1636625e63baea635 Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Wed, 13 Sep 2023 14:59:20 +0200 Subject: [PATCH 1/6] feat: Added usePromise hook --- .../pages/react-hooks/hooks/use-promise.mdx | 35 +++++++++++ .../react-hooks/changes/Added usePromise hook | 0 packages/react-hooks/hooks/usePromise.ts | 60 +++++++++++++++++++ packages/react-hooks/index.ts | 1 + 4 files changed, 96 insertions(+) create mode 100644 apps/docs/pages/react-hooks/hooks/use-promise.mdx create mode 100644 packages/react-hooks/changes/Added usePromise hook create mode 100644 packages/react-hooks/hooks/usePromise.ts diff --git a/apps/docs/pages/react-hooks/hooks/use-promise.mdx b/apps/docs/pages/react-hooks/hooks/use-promise.mdx new file mode 100644 index 00000000..a3c6a63f --- /dev/null +++ b/apps/docs/pages/react-hooks/hooks/use-promise.mdx @@ -0,0 +1,35 @@ +--- +title: usePromise +--- + +import { usePromise } from '@enterwell/react-hooks'; +import { ComponentDescription, ComponentParameters, ComponentSource } from '../../../components/docs/ComponentDocs'; + +# usePromise + +## Description + + + +### Parameters + + + +## Example + +```ts filename="example.ts" +import { usePromise } from '@enterwell/react-hooks'; + +const { item, isLoading, error } = usePromise(getData); +``` + +## Inspect + +
+ Source code + +
diff --git a/packages/react-hooks/changes/Added usePromise hook b/packages/react-hooks/changes/Added usePromise hook new file mode 100644 index 00000000..e69de29b diff --git a/packages/react-hooks/hooks/usePromise.ts b/packages/react-hooks/hooks/usePromise.ts new file mode 100644 index 00000000..a90ee8e9 --- /dev/null +++ b/packages/react-hooks/hooks/usePromise.ts @@ -0,0 +1,60 @@ +import { + useEffect, + useRef, + useState, + useTransition +} from 'react'; + +/** + * The result of a usePromise hook. + * @public + */ +export type UsePromiseResult = { + item?: T | undefined; + isLoading: boolean; + error?: string | undefined; +}; + +/** + * A function that returns a promise or undefined. + * @public + */ +export type PromiseFunction = (Promise | undefined) | (() => Promise | undefined); + +/** + * The hook is used to load data and handle loading and error states. + * + * @param promise - The promise function to call. If it returns a promise, the promise will be awaited. If it returns undefined, the load is considered complete. + * @returns An object with the current state of the load operation. + * @public + */ +export function usePromise(promise?: PromiseFunction): UsePromiseResult { + const [state, setState] = useState>({ isLoading: true, item: undefined, error: undefined }); + const [, startTransition] = useTransition(); + const loadPromiseRef = useRef>(); + + useEffect(() => { + (async () => { + try { + if (!promise || loadPromiseRef.current) { + return; + } + + setState({ isLoading: true }); + + loadPromiseRef.current = typeof promise === 'function' ? promise() : promise; + const item = await loadPromiseRef.current; + + startTransition(() => { + setState({ isLoading: false, item }); + loadPromiseRef.current = undefined; + }); + } catch (err: any) { + setState({ isLoading: false, error: err?.toString() }); + loadPromiseRef.current = undefined; + } + })(); + }, [promise]); + + return state; +} \ No newline at end of file diff --git a/packages/react-hooks/index.ts b/packages/react-hooks/index.ts index a905f144..af9aed55 100644 --- a/packages/react-hooks/index.ts +++ b/packages/react-hooks/index.ts @@ -1,4 +1,5 @@ // hook exports +export * from "./hooks/usePromise"; export * from "./hooks/useDebouncedEffect"; export * from "./hooks/useDebounce"; export * from "./hooks/useIsomorphicLayoutEffect"; From 110296b1dac4c0ee5703fc2415bff386c70a426e Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Wed, 13 Sep 2023 15:00:01 +0200 Subject: [PATCH 2/6] Update react-hooks.api.md --- packages/react-hooks/temp/react-hooks.api.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/react-hooks/temp/react-hooks.api.md b/packages/react-hooks/temp/react-hooks.api.md index faedebcb..47f5d3e0 100644 --- a/packages/react-hooks/temp/react-hooks.api.md +++ b/packages/react-hooks/temp/react-hooks.api.md @@ -7,6 +7,9 @@ import * as react from 'react'; import { useEffect } from 'react'; +// @public +export type PromiseFunction = (Promise | undefined) | (() => Promise | undefined); + // @public export function useDebounce(value: T, delay: number): T; @@ -16,6 +19,16 @@ export function useDebouncedEffect(effect: Function, deps: unknown[], delay: num // @public export const useIsomorphicLayoutEffect: typeof useEffect; +// @public +export function usePromise(promise?: PromiseFunction): UsePromiseResult; + +// @public +export type UsePromiseResult = { + item?: T | undefined; + isLoading: boolean; + error?: string | undefined; +}; + // @public export function useResizeObserver(callback: (element: any, entry: ResizeObserverEntry) => void): react.MutableRefObject; From 0a4e4ed3f203d2b445ea40b809d8489885ed9fe4 Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Thu, 14 Sep 2023 12:39:55 +0200 Subject: [PATCH 3/6] feat: Added cancellation ability to usePromise, improved comments --- .../pages/react-hooks/hooks/use-promise.mdx | 54 +++++++++++++++- packages/react-hooks/hooks/usePromise.ts | 64 ++++++++++++++----- 2 files changed, 99 insertions(+), 19 deletions(-) diff --git a/apps/docs/pages/react-hooks/hooks/use-promise.mdx b/apps/docs/pages/react-hooks/hooks/use-promise.mdx index a3c6a63f..d082c8ab 100644 --- a/apps/docs/pages/react-hooks/hooks/use-promise.mdx +++ b/apps/docs/pages/react-hooks/hooks/use-promise.mdx @@ -15,11 +15,61 @@ import { ComponentDescription, ComponentParameters, ComponentSource } from '../. -## Example +## Examples -```ts filename="example.ts" +```ts filename="example.ts - Example with function that returns promise" {8} import { usePromise } from '@enterwell/react-hooks'; +const getData = async () => { + const response = await fetch('https://jsonplaceholder.typicode.com/todos'); + return await response.json(); +}; + +const { item, isLoading, error } = usePromise(getData); +``` + +```ts filename="example.ts - Example with function that accepts argument" {10-11} +import { useCallback } from 'react'; +import { usePromise } from '@enterwell/react-hooks'; + +const getData = async (id: number | undefined) => { + const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`); + return await response.json(); +}; + +const [id, setId] = useState(undefined); +const getDataCallback = useCallback(() => getData(id), [id]); +const { item, isLoading, error } = usePromise(getDataCallback); +``` + +```ts filename="example.ts - Example with function that accepts argument, disabled (in loading state) until 'id' is set" {10-11} +import { useMemo } from 'react'; +import { usePromise } from '@enterwell/react-hooks'; + +const getData = async (id: number) => { + const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`); + return await response.json(); +}; + +const [id, setId] = useState(undefined); +const getDataCallback = useMemo(() => id ? () => getData(id) : undefined, [id]); +const { item, isLoading, error } = usePromise(getDataCallback); +``` + +```ts filename="example.ts - Example with promise object" {14} +import { usePromise } from '@enterwell/react-hooks'; + +const getData = () => { + return new Promise(async (resolve, reject) => { + try { + const response = await fetch('https://jsonplaceholder.typicode.com/todos'); + resolve(await response.json()); + } catch(err) { + reject(err); + } + }); +}; + const { item, isLoading, error } = usePromise(getData); ``` diff --git a/packages/react-hooks/hooks/usePromise.ts b/packages/react-hooks/hooks/usePromise.ts index a90ee8e9..930256b7 100644 --- a/packages/react-hooks/hooks/usePromise.ts +++ b/packages/react-hooks/hooks/usePromise.ts @@ -22,38 +22,68 @@ export type UsePromiseResult = { export type PromiseFunction = (Promise | undefined) | (() => Promise | undefined); /** - * The hook is used to load data and handle loading and error states. + * The hook is used to load data and handle loading and error states. * - * @param promise - The promise function to call. If it returns a promise, the promise will be awaited. If it returns undefined, the load is considered complete. - * @returns An object with the current state of the load operation. + * @param promise - The function that returns promise or promise object to await.
+ * - If `promise` argument is not provided ([falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy)), the load is paused in loading state until `promise` is provided.
+ * - If the function returns a promise or promise object is passed, the promise will be awaited.
+ * - If the function returns undefined, the load is considered complete and empty. + * @returns An object with the current state of the operation. * @public */ export function usePromise(promise?: PromiseFunction): UsePromiseResult { const [state, setState] = useState>({ isLoading: true, item: undefined, error: undefined }); const [, startTransition] = useTransition(); - const loadPromiseRef = useRef>(); + const loadPromiseRef = useRef | undefined>(undefined); useEffect(() => { - (async () => { - try { - if (!promise || loadPromiseRef.current) { - return; - } + let canceled = false; + + // Ignore if promise not provided or already loading and not canceled + if (!promise || (loadPromiseRef.current && !canceled)) { + return; + } + + setState((curr) => ({ ...curr, isLoading: true })); - setState({ isLoading: true }); + console.log('usePromise', 'promiseProvided', JSON.stringify(promise), typeof promise, !promise) - loadPromiseRef.current = typeof promise === 'function' ? promise() : promise; - const item = await loadPromiseRef.current; + // Resolve as promise object or function + loadPromiseRef.current = typeof promise === 'function' ? promise() : promise; + // If promise is undefined, load is considered complete and empty + if (!loadPromiseRef.current) { + startTransition(() => { + setState({ isLoading: false }); + }); + return; + } + + loadPromiseRef.current + .then(item => { + loadPromiseRef.current = undefined; + if (canceled) { + return; + } startTransition(() => { setState({ isLoading: false, item }); - loadPromiseRef.current = undefined; }); - } catch (err: any) { - setState({ isLoading: false, error: err?.toString() }); + }).catch(err => { loadPromiseRef.current = undefined; - } - })(); + if (canceled) { + return; + } + startTransition(() => { + setState({ + isLoading: false, + error: Boolean(err) && 'toString' in err ? err.toString() : undefined + }); + }); + }); + + return () => { + canceled = true; + }; }, [promise]); return state; From 2606d82f145da800c169b51033ce0fc2708a1d2f Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Fri, 15 Sep 2023 13:04:12 +0200 Subject: [PATCH 4/6] Update usePromise.ts --- packages/react-hooks/hooks/usePromise.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react-hooks/hooks/usePromise.ts b/packages/react-hooks/hooks/usePromise.ts index 930256b7..932e4c31 100644 --- a/packages/react-hooks/hooks/usePromise.ts +++ b/packages/react-hooks/hooks/usePromise.ts @@ -46,8 +46,6 @@ export function usePromise(promise?: PromiseFunction): UsePromiseResult setState((curr) => ({ ...curr, isLoading: true })); - console.log('usePromise', 'promiseProvided', JSON.stringify(promise), typeof promise, !promise) - // Resolve as promise object or function loadPromiseRef.current = typeof promise === 'function' ? promise() : promise; @@ -87,4 +85,4 @@ export function usePromise(promise?: PromiseFunction): UsePromiseResult }, [promise]); return state; -} \ No newline at end of file +} From 04263e373f431c5b98c2bad6f00f8873d94ba1f1 Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Fri, 15 Sep 2023 13:47:11 +0200 Subject: [PATCH 5/6] Update usePromise.ts --- packages/react-hooks/hooks/usePromise.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/react-hooks/hooks/usePromise.ts b/packages/react-hooks/hooks/usePromise.ts index 932e4c31..341bfba0 100644 --- a/packages/react-hooks/hooks/usePromise.ts +++ b/packages/react-hooks/hooks/usePromise.ts @@ -1,6 +1,5 @@ import { useEffect, - useRef, useState, useTransition } from 'react'; @@ -34,32 +33,30 @@ export type PromiseFunction = (Promise | undefined) | (() => Promise | export function usePromise(promise?: PromiseFunction): UsePromiseResult { const [state, setState] = useState>({ isLoading: true, item: undefined, error: undefined }); const [, startTransition] = useTransition(); - const loadPromiseRef = useRef | undefined>(undefined); useEffect(() => { let canceled = false; - // Ignore if promise not provided or already loading and not canceled - if (!promise || (loadPromiseRef.current && !canceled)) { + // Ignore if promise not provided + if (!promise) { return; } setState((curr) => ({ ...curr, isLoading: true })); // Resolve as promise object or function - loadPromiseRef.current = typeof promise === 'function' ? promise() : promise; + const current = typeof promise === 'function' ? promise() : promise; // If promise is undefined, load is considered complete and empty - if (!loadPromiseRef.current) { + if (!current) { startTransition(() => { setState({ isLoading: false }); }); return; } - loadPromiseRef.current + current .then(item => { - loadPromiseRef.current = undefined; if (canceled) { return; } @@ -67,7 +64,6 @@ export function usePromise(promise?: PromiseFunction): UsePromiseResult setState({ isLoading: false, item }); }); }).catch(err => { - loadPromiseRef.current = undefined; if (canceled) { return; } From ee4962eaee2c61b97f2905b3bb2d2a2ec799034b Mon Sep 17 00:00:00 2001 From: Aleksandar Toplek Date: Fri, 15 Sep 2023 13:50:08 +0200 Subject: [PATCH 6/6] Update use-promise.mdx --- apps/docs/pages/react-hooks/hooks/use-promise.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/docs/pages/react-hooks/hooks/use-promise.mdx b/apps/docs/pages/react-hooks/hooks/use-promise.mdx index d082c8ab..6cd8fb89 100644 --- a/apps/docs/pages/react-hooks/hooks/use-promise.mdx +++ b/apps/docs/pages/react-hooks/hooks/use-promise.mdx @@ -70,7 +70,8 @@ const getData = () => { }); }; -const { item, isLoading, error } = usePromise(getData); +const getDataPromise = getData(); +const { item, isLoading, error } = usePromise(getDataPromise); ``` ## Inspect