From d5a1be3b0a04de2e016217600d0b0ac342353a4a Mon Sep 17 00:00:00 2001 From: danybeltran Date: Wed, 8 Feb 2023 20:36:23 -0600 Subject: [PATCH 1/3] feat(performance): - Improved useFetch performance - 'useFetch' will initialize revalidation only when 'data' is accessed --- package.json | 2 +- src/hooks/others.ts | 26 +++-- src/hooks/use-fetch.ts | 228 +++++++++++++++++++++++++---------------- src/internal/index.ts | 3 +- 4 files changed, 159 insertions(+), 100 deletions(-) diff --git a/package.json b/package.json index 60799a9..aaff097 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "http-react", - "version": "3.0.2", + "version": "3.0.3", "description": "React hooks for data fetching", "main": "dist/index.js", "scripts": { diff --git a/src/hooks/others.ts b/src/hooks/others.ts index f116948..7e413cd 100644 --- a/src/hooks/others.ts +++ b/src/hooks/others.ts @@ -126,13 +126,17 @@ export function useFetchCode(id: any) { * Get the loading state of a request using its id */ export function useFetchLoading(id: any): boolean { - const idString = serialize({ idString: serialize(id) }) - - const { loading } = useFetch({ - id: id + const { loading, ...all } = useFetch({ + id }) - - return !isDefined(runningRequests[idString]) ? true : isPending(idString) + return ( + loading && + isPending( + serialize({ + idString: serialize(id) + }) + ) + ) } /** @@ -159,11 +163,11 @@ export function useFetchError(id: any, onError?: (err?: any) => void) { } }, [resolvedKey]) - const { error } = useFetch({ - id: id - }) - - return error + return ( + useFetch({ + id: id + }).error || hasErrors[resolvedKey] + ) } /** diff --git a/src/hooks/use-fetch.ts b/src/hooks/use-fetch.ts index c2e22bd..a8d2651 100644 --- a/src/hooks/use-fetch.ts +++ b/src/hooks/use-fetch.ts @@ -322,9 +322,7 @@ export function useFetch( loading: auto ? isPending(resolvedKey) || (revalidateOnMount - ? suspense - ? isPending(resolvedKey) - : true + ? previousConfig[resolvedKey] !== serialize(optionsConfig) : previousConfig[resolvedKey] !== serialize(optionsConfig)) : false, error: (hasErrors[resolvedDataKey] || false) as boolean, @@ -332,13 +330,22 @@ export function useFetch( }) const thisDeps = React.useRef({ - data: false, - online: false, - loading: false, - error: false, - completedAttempts: false + data: undefined as unknown as boolean, + online: undefined as unknown as boolean, + loading: undefined as unknown as boolean, + error: undefined as unknown as boolean, + completedAttempts: undefined as unknown as boolean }) + const inDeps = (k: keyof typeof thisDeps.current) => { + return !isDefined(thisDeps.current[k]) || thisDeps.current[k] + } + const setDep = (k: keyof typeof thisDeps.current) => { + if (!isDefined(thisDeps.current[k])) { + thisDeps.current[k] = false + } + } + const { data, loading, online, error, completedAttempts } = fetchState const isLoading = isExpired ? isPending(resolvedKey) || loading : false @@ -833,32 +840,10 @@ export function useFetch( } } } finally { - setFetchState(p => { - const n = { - ...p, - data: thisDeps.current.data ? $$data ?? p.data : p.data, - online: thisDeps.current.online ? p.online : p.online, - loading: thisDeps.current.loading - ? rpc?.loading ?? false - : p.loading, - error: thisDeps.current.error - ? isDefined($$error) - ? $$error - : p.error - : p.error, - completedAttempts: thisDeps.current.completedAttempts - ? $$completedAttempts ?? p.completedAttempts - : p.completedAttempts - } - if (jsonCompare(n, p)) return p - return n - }) - runningRequests[resolvedKey] = false suspenseInitialized[resolvedKey] = true requestsProvider.emit(resolvedKey, { - requestCallId, error: hasErrors[resolvedKey] || hasErrors[resolvedDataKey] || false, ...rpc, @@ -875,7 +860,11 @@ export function useFetch( } }, [ - thisDeps.current, + thisDeps.current.data, + thisDeps.current.online, + thisDeps.current.loading, + thisDeps.current.error, + thisDeps.current.completedAttempts, canRevalidate, ctx.auto, stringDeps, @@ -960,28 +949,21 @@ export function useFetch( if (v.requestCallId !== requestCallId) { if (!willSuspend[resolvedKey]) { queue(() => { - setFetchState(p => { - const n = { - ...p, - data: thisDeps.current.data - ? !jsonCompare($data, p.data) - ? $data - : p.data ?? p.data - : p.data, - online: thisDeps.current.online ? online ?? p.online : p.online, - loading: thisDeps.current.loading - ? loading ?? p.loading - : p.loading, - error: thisDeps.current.error ? Boolean($error) : p.error, - completedAttempts: thisDeps.current.completedAttempts - ? completedAttempts ?? p.completedAttempts - : p.completedAttempts - } - - if (jsonCompare(n, p)) return p - - return n - }) + if (inDeps('data')) { + setData($data) + } + if (inDeps('online')) { + setOnline(online) + } + if (inDeps('loading')) { + setLoading(loading) + } + if (inDeps('error')) { + setData($error) + } + if (inDeps('completedAttempts')) { + setCompletedAttempts(completedAttempts) + } }) } } @@ -993,7 +975,11 @@ export function useFetch( requestsProvider.removeListener(resolvedKey, waitFormUpdates) } }, [ - thisDeps.current, + thisDeps.current.data, + thisDeps.current.online, + thisDeps.current.loading, + thisDeps.current.error, + thisDeps.current.completedAttempts, JSON.stringify(optionsConfig), resolvedKey, resolvedDataKey, @@ -1172,7 +1158,7 @@ export function useFetch( online: false, error: true }) - if (thisDeps.current.online) setOnline(false) + if (inDeps('online')) setOnline(false) } } }, getMiliseconds(attemptInterval as TimeSpan)) @@ -1201,31 +1187,33 @@ export function useFetch( const initializeRevalidation = React.useCallback( async function initializeRevalidation() { - let d = undefined - if (canRevalidate) { - if (url !== '') { - d = await fetchData({ - query: Object.keys(reqQuery) - .map(q => [q, reqQuery[q]].join('=')) - .join('&'), - params: reqParams - }) + if (inDeps('data')) { + let d = undefined + if (canRevalidate) { + if (url !== '') { + d = await fetchData({ + query: Object.keys(reqQuery) + .map(q => [q, reqQuery[q]].join('=')) + .join('&'), + params: reqParams + }) + } else { + d = def + // It means a url is not passed + setFetchState(prev => ({ + ...prev, + loading: false, + error: hasErrors[resolvedDataKey] || hasErrors[resolvedKey], + completedAttempts: prev.completedAttempts + })) + } } else { d = def - // It means a url is not passed - setFetchState(prev => ({ - ...prev, - loading: false, - error: hasErrors[resolvedDataKey] || hasErrors[resolvedKey], - completedAttempts: prev.completedAttempts - })) } - } else { - d = def + return d } - return d }, - [serialize(serialize(optionsConfig)), fetchState] + [serialize(serialize(optionsConfig)), fetchState, thisDeps.current.data] ) if (!suspense) { @@ -1243,18 +1231,21 @@ export function useFetch( }, [serialize(optionsConfig)]) if (suspense) { - if (auto) { - if (windowExists) { - if (!suspenseInitialized[resolvedKey]) { - if (!suspenseRevalidationStarted[resolvedKey]) { - suspenseRevalidationStarted[resolvedKey] = initializeRevalidation() + if (inDeps('data')) { + if (auto) { + if (windowExists) { + if (!suspenseInitialized[resolvedKey]) { + if (!suspenseRevalidationStarted[resolvedKey]) { + suspenseRevalidationStarted[resolvedKey] = + initializeRevalidation() + } + throw suspenseRevalidationStarted[resolvedKey] + } + } else { + throw { + message: + "Use 'SSRSuspense' instead of 'Suspense' when using SSR and suspense" } - throw suspenseRevalidationStarted[resolvedKey] - } - } else { - throw { - message: - "Use 'SSRSuspense' instead of 'Suspense' when using SSR and suspense" } } } @@ -1411,7 +1402,7 @@ export function useFetch( ? new Date(cacheProvider.get('expiration' + resolvedDataKey)) : null - const isFailed = hasErrors[resolvedDataKey] || error + const isFailed = hasErrors[resolvedDataKey] || hasErrors[resolvedKey] || error const responseData = (error && isFailed ? (cacheIfError ? thisCache : null) : thisCache) ?? def @@ -1427,63 +1418,122 @@ export function useFetch( return { get revalidating() { thisDeps.current.loading = true + setDep('data') + setDep('online') + setDep('error') + setDep('completedAttempts') return oneRequestResolved && isLoading }, get hasData() { thisDeps.current.data = true + setDep('loading') + setDep('online') + setDep('error') + setDep('completedAttempts') return oneRequestResolved }, get success() { thisDeps.current.loading = true thisDeps.current.error = true + setDep('data') + setDep('online') + setDep('completedAttempts') return isSuccess }, get loadingFirst() { thisDeps.current.loading = true + setDep('data') + setDep('online') + setDep('error') + setDep('completedAttempts') return loadingFirst }, get requestStart() { thisDeps.current.loading = true + setDep('data') + setDep('online') + setDep('error') + setDep('completedAttempts') return getDateIfValid($requestStart) }, get requestEnd() { thisDeps.current.loading = true + setDep('data') + setDep('online') + setDep('error') + setDep('completedAttempts') return getDateIfValid($requestEnd) }, get expiration() { thisDeps.current.loading = true + setDep('data') + setDep('online') + setDep('error') + setDep('completedAttempts') return getDateIfValid(isFailed ? null : expirationDate) }, get responseTime() { thisDeps.current.loading = true + setDep('data') + setDep('online') + setDep('error') + setDep('completedAttempts') return requestResponseTimes[resolvedDataKey] ?? null }, get data() { thisDeps.current.data = true + setDep('loading') + setDep('online') + setDep('error') + setDep('completedAttempts') return responseData }, get loading() { thisDeps.current.loading = true + setDep('data') + setDep('online') + setDep('error') + setDep('completedAttempts') return isLoading }, get error() { thisDeps.current.error = true + setDep('loading') + setDep('data') + setDep('online') + setDep('completedAttempts') return isFailed || false }, get online() { thisDeps.current.online = true + setDep('loading') + setDep('data') + setDep('error') + setDep('completedAttempts') return online }, get code() { thisDeps.current.loading = true + setDep('data') + setDep('online') + setDep('error') + setDep('completedAttempts') return statusCodes[resolvedKey] }, get reFetch() { thisDeps.current.loading = true + setDep('data') + setDep('online') + setDep('error') + setDep('completedAttempts') return reValidate }, get mutate() { thisDeps.current.data = true + setDep('loading') + setDep('online') + setDep('error') + setDep('completedAttempts') return forceMutate }, get fetcher() { @@ -1509,6 +1559,10 @@ export function useFetch( config: __config, get response() { thisDeps.current.loading = true + setDep('data') + setDep('online') + setDep('error') + setDep('completedAttempts') return lastResponses[resolvedKey] }, id, diff --git a/src/internal/index.ts b/src/internal/index.ts index 8dc1868..007ecb5 100644 --- a/src/internal/index.ts +++ b/src/internal/index.ts @@ -168,7 +168,8 @@ const defaultContextVaue: FetchContextType = { online: ONLINE, retryOnReconnect: RETRY_ON_RECONNECT, revalidateOnMount: REVALIDATE_ON_MOUNT, - cacheIfError: true + cacheIfError: true, + maxCacheAge: '250 ms' } export const FetchContext = createContext(defaultContextVaue) From db3f6f0ea657118e929a3451055d8d67338f3786 Mon Sep 17 00:00:00 2001 From: danybeltran Date: Thu, 9 Feb 2023 10:25:15 -0600 Subject: [PATCH 2/3] feat(data): - Revalidation will start only when 'data' is accesed --- src/hooks/use-fetch.ts | 226 ++++++++++++++--------------------------- src/internal/index.ts | 3 +- 2 files changed, 77 insertions(+), 152 deletions(-) diff --git a/src/hooks/use-fetch.ts b/src/hooks/use-fetch.ts index a8d2651..0536fe3 100644 --- a/src/hooks/use-fetch.ts +++ b/src/hooks/use-fetch.ts @@ -330,20 +330,15 @@ export function useFetch( }) const thisDeps = React.useRef({ - data: undefined as unknown as boolean, - online: undefined as unknown as boolean, - loading: undefined as unknown as boolean, - error: undefined as unknown as boolean, - completedAttempts: undefined as unknown as boolean - }) - - const inDeps = (k: keyof typeof thisDeps.current) => { - return !isDefined(thisDeps.current[k]) || thisDeps.current[k] - } - const setDep = (k: keyof typeof thisDeps.current) => { - if (!isDefined(thisDeps.current[k])) { - thisDeps.current[k] = false - } + data: false, + online: false, + loading: false, + error: false, + completedAttempts: false + }).current + + const inDeps = (k: keyof typeof thisDeps) => { + return thisDeps[k] } const { data, loading, online, error, completedAttempts } = fetchState @@ -860,11 +855,7 @@ export function useFetch( } }, [ - thisDeps.current.data, - thisDeps.current.online, - thisDeps.current.loading, - thisDeps.current.error, - thisDeps.current.completedAttempts, + thisDeps, canRevalidate, ctx.auto, stringDeps, @@ -959,7 +950,7 @@ export function useFetch( setLoading(loading) } if (inDeps('error')) { - setData($error) + setError($error) } if (inDeps('completedAttempts')) { setCompletedAttempts(completedAttempts) @@ -975,11 +966,7 @@ export function useFetch( requestsProvider.removeListener(resolvedKey, waitFormUpdates) } }, [ - thisDeps.current.data, - thisDeps.current.online, - thisDeps.current.loading, - thisDeps.current.error, - thisDeps.current.completedAttempts, + thisDeps, JSON.stringify(optionsConfig), resolvedKey, resolvedDataKey, @@ -1187,33 +1174,31 @@ export function useFetch( const initializeRevalidation = React.useCallback( async function initializeRevalidation() { - if (inDeps('data')) { - let d = undefined - if (canRevalidate) { - if (url !== '') { - d = await fetchData({ - query: Object.keys(reqQuery) - .map(q => [q, reqQuery[q]].join('=')) - .join('&'), - params: reqParams - }) - } else { - d = def - // It means a url is not passed - setFetchState(prev => ({ - ...prev, - loading: false, - error: hasErrors[resolvedDataKey] || hasErrors[resolvedKey], - completedAttempts: prev.completedAttempts - })) - } + let d = undefined + if (canRevalidate) { + if (url !== '') { + d = await fetchData({ + query: Object.keys(reqQuery) + .map(q => [q, reqQuery[q]].join('=')) + .join('&'), + params: reqParams + }) } else { d = def + // It means a url is not passed + setFetchState(prev => ({ + ...prev, + loading: false, + error: hasErrors[resolvedDataKey] || hasErrors[resolvedKey], + completedAttempts: prev.completedAttempts + })) } - return d + } else { + d = def } + return d }, - [serialize(serialize(optionsConfig)), fetchState, thisDeps.current.data] + [serialize(serialize(optionsConfig)), fetchState, thisDeps] ) if (!suspense) { @@ -1221,43 +1206,45 @@ export function useFetch( suspenseInitialized[resolvedKey] = true } } - useEffect(() => { + + React.useLayoutEffect(() => { if (url !== '') { if (!jsonCompare(previousProps[resolvedKey], optionsConfig)) { abortControllers[resolvedKey]?.abort() - queue(initializeRevalidation) + if (inDeps('data')) { + queue(initializeRevalidation) + } } } - }, [serialize(optionsConfig)]) + }, [serialize(optionsConfig), thisDeps]) if (suspense) { - if (inDeps('data')) { - if (auto) { - if (windowExists) { - if (!suspenseInitialized[resolvedKey]) { - if (!suspenseRevalidationStarted[resolvedKey]) { - suspenseRevalidationStarted[resolvedKey] = - initializeRevalidation() - } - throw suspenseRevalidationStarted[resolvedKey] - } - } else { - throw { - message: - "Use 'SSRSuspense' instead of 'Suspense' when using SSR and suspense" + if (auto) { + if (windowExists) { + if (!suspenseInitialized[resolvedKey]) { + if (!suspenseRevalidationStarted[resolvedKey]) { + suspenseRevalidationStarted[resolvedKey] = initializeRevalidation() } + throw suspenseRevalidationStarted[resolvedKey] + } + } else { + throw { + message: + "Use 'SSRSuspense' instead of 'Suspense' when using SSR and suspense" } } } } - React.useMemo(() => { + React.useLayoutEffect(() => { if (!runningRequests[resolvedKey] && isExpired) { if (windowExists) { if (canRevalidate && url !== '') { if (!jsonCompare(previousConfig[resolvedKey], optionsConfig)) { if (!isPending(resolvedKey)) { - initializeRevalidation() + if (inDeps('data')) { + initializeRevalidation() + } } else { setLoading(true) } @@ -1265,16 +1252,18 @@ export function useFetch( } } } - }, [resolvedKey, serialize(optionsConfig), canRevalidate]) + }, [resolvedKey, serialize(optionsConfig), canRevalidate, thisDeps]) - useEffect(() => { + React.useLayoutEffect(() => { const revalidateAfterUnmount = revalidateOnMount ? true : previousConfig[resolvedKey] !== serialize(optionsConfig) function revalidate() { if (!debounce && !canDebounce[resolvedKey]) { - initializeRevalidation() + if (inDeps('data')) { + initializeRevalidation() + } } } @@ -1289,7 +1278,7 @@ export function useFetch( } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [serialize(optionsConfig)]) + }, [serialize(optionsConfig), thisDeps]) useEffect(() => { function addFocusListener() { @@ -1417,123 +1406,64 @@ export function useFetch( return { get revalidating() { - thisDeps.current.loading = true - setDep('data') - setDep('online') - setDep('error') - setDep('completedAttempts') + thisDeps.loading = true return oneRequestResolved && isLoading }, get hasData() { - thisDeps.current.data = true - setDep('loading') - setDep('online') - setDep('error') - setDep('completedAttempts') + thisDeps.data = true return oneRequestResolved }, get success() { - thisDeps.current.loading = true - thisDeps.current.error = true - setDep('data') - setDep('online') - setDep('completedAttempts') + thisDeps.loading = true + thisDeps.error = true return isSuccess }, get loadingFirst() { - thisDeps.current.loading = true - setDep('data') - setDep('online') - setDep('error') - setDep('completedAttempts') + thisDeps.loading = true return loadingFirst }, get requestStart() { - thisDeps.current.loading = true - setDep('data') - setDep('online') - setDep('error') - setDep('completedAttempts') + thisDeps.loading = true return getDateIfValid($requestStart) }, get requestEnd() { - thisDeps.current.loading = true - setDep('data') - setDep('online') - setDep('error') - setDep('completedAttempts') + thisDeps.loading = true return getDateIfValid($requestEnd) }, get expiration() { - thisDeps.current.loading = true - setDep('data') - setDep('online') - setDep('error') - setDep('completedAttempts') + thisDeps.loading = true return getDateIfValid(isFailed ? null : expirationDate) }, get responseTime() { - thisDeps.current.loading = true - setDep('data') - setDep('online') - setDep('error') - setDep('completedAttempts') + thisDeps.loading = true return requestResponseTimes[resolvedDataKey] ?? null }, get data() { - thisDeps.current.data = true - setDep('loading') - setDep('online') - setDep('error') - setDep('completedAttempts') + thisDeps.data = true return responseData }, get loading() { - thisDeps.current.loading = true - setDep('data') - setDep('online') - setDep('error') - setDep('completedAttempts') + thisDeps.loading = true return isLoading }, get error() { - thisDeps.current.error = true - setDep('loading') - setDep('data') - setDep('online') - setDep('completedAttempts') + thisDeps.error = true return isFailed || false }, get online() { - thisDeps.current.online = true - setDep('loading') - setDep('data') - setDep('error') - setDep('completedAttempts') + thisDeps.online = true return online }, get code() { - thisDeps.current.loading = true - setDep('data') - setDep('online') - setDep('error') - setDep('completedAttempts') + thisDeps.loading = true return statusCodes[resolvedKey] }, get reFetch() { - thisDeps.current.loading = true - setDep('data') - setDep('online') - setDep('error') - setDep('completedAttempts') + thisDeps.loading = true return reValidate }, get mutate() { - thisDeps.current.data = true - setDep('loading') - setDep('online') - setDep('error') - setDep('completedAttempts') + thisDeps.data = true return forceMutate }, get fetcher() { @@ -1558,11 +1488,7 @@ export function useFetch( }, config: __config, get response() { - thisDeps.current.loading = true - setDep('data') - setDep('online') - setDep('error') - setDep('completedAttempts') + thisDeps.loading = true return lastResponses[resolvedKey] }, id, diff --git a/src/internal/index.ts b/src/internal/index.ts index 007ecb5..8dc1868 100644 --- a/src/internal/index.ts +++ b/src/internal/index.ts @@ -168,8 +168,7 @@ const defaultContextVaue: FetchContextType = { online: ONLINE, retryOnReconnect: RETRY_ON_RECONNECT, revalidateOnMount: REVALIDATE_ON_MOUNT, - cacheIfError: true, - maxCacheAge: '250 ms' + cacheIfError: true } export const FetchContext = createContext(defaultContextVaue) From 4a8204aa6a081ed74009ce55677cf13139afd61a Mon Sep 17 00:00:00 2001 From: danybeltran Date: Thu, 9 Feb 2023 10:26:11 -0600 Subject: [PATCH 3/3] update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aaff097..da55abd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "http-react", - "version": "3.0.3", + "version": "3.0.4", "description": "React hooks for data fetching", "main": "dist/index.js", "scripts": {