From dd54ec5f493691dafb1663c74fcbe6347409322c Mon Sep 17 00:00:00 2001 From: danybeltran Date: Wed, 11 Dec 2024 15:56:27 -0600 Subject: [PATCH] features(transform): Adds the transform API. - Similar to 'middleware' but it transforms data on the fly without making a new request (happens instantly) - 'transform' first checks data is not undefined before running, which makes sure it only runs when data or fallback data exist, preventing (most) runtime errors that could arise when manipulatind data that could be undefined --- package.json | 2 +- src/components/server/index.tsx | 8 +- src/hooks/use-fetch.ts | 389 ++++++++++++++++---------------- src/internal/constants.ts | 28 +-- src/internal/index.ts | 20 +- src/types/index.ts | 40 ++-- 6 files changed, 249 insertions(+), 238 deletions(-) diff --git a/package.json b/package.json index 6fb3e6e..6860fe8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "http-react", - "version": "3.7.3", + "version": "3.7.4", "description": "React hooks for data fetching", "main": "dist/index.js", "scripts": { diff --git a/src/components/server/index.tsx b/src/components/server/index.tsx index 94c23a1..ed5242d 100644 --- a/src/components/server/index.tsx +++ b/src/components/server/index.tsx @@ -1,9 +1,9 @@ -"use client" +'use client' -import { FetchConfigAsync } from "../FetchConfigAsync" +import { FetchConfigAsync } from '../FetchConfigAsync' -import { FetchConfig as FConfig, SSRSuspense } from "../index" +import { FetchConfig as FConfig, SSRSuspense } from '../index' -const FetchConfig = typeof window === "undefined" ? FetchConfigAsync : FConfig +const FetchConfig = typeof window === 'undefined' ? FetchConfigAsync : FConfig export { FetchConfig, SSRSuspense, FConfig as FetchConfigSync } diff --git a/src/hooks/use-fetch.ts b/src/hooks/use-fetch.ts index 033c28e..cd84c02 100644 --- a/src/hooks/use-fetch.ts +++ b/src/hooks/use-fetch.ts @@ -1,5 +1,5 @@ -"use client" -import { useState, useEffect, useMemo, useRef, useCallback } from "react" +'use client' +import { useState, useEffect, useMemo, useRef, useCallback } from 'react' import { abortControllers, @@ -29,10 +29,10 @@ import { suspenseRevalidationStarted, onlineHandled, offlineHandled, - hasData, -} from "../internal" + hasData +} from '../internal' -import { DEFAULT_RESOLVER, METHODS } from "../internal/constants" +import { DEFAULT_RESOLVER, METHODS } from '../internal/constants' import { CustomResponse, @@ -41,16 +41,16 @@ import { FetchContextType, HTTP_METHODS, ImperativeFetch, - TimeSpan, -} from "../types" + TimeSpan +} from '../types' import { createImperativeFetch, getMiliseconds, getTimePassed, revalidate, - useIsomorphicLayoutEffect, -} from "../utils" + useIsomorphicLayoutEffect +} from '../utils' import { createRequestFn, getRequestHeaders, @@ -63,16 +63,16 @@ import { queue, serialize, setURLParams, - windowExists, -} from "../utils/shared" -import { $context } from "../internal/shared" + windowExists +} from '../utils/shared' +import { $context } from '../internal/shared' /** * Passing `undefined` to `new Date()` returns `Invalid Date {}`, so return null instead */ const getDateIfValid = (d: Date | null) => // @ts-ignore - Evals to a Date - (d?.toString() === "Invalid Date" || d === null ? null : d) as Date + (d?.toString() === 'Invalid Date' || d === null ? null : d) as Date /** * Termporary form data is set with the submit method in useFetch and is deleted immediately after resolving (see line #858) @@ -93,12 +93,12 @@ export function useFetch( ...$ctx, query: { ...$context?.value?.query, - ...$ctx?.query, + ...$ctx?.query }, headers: { ...$context.value?.headers, - ...$ctx?.headers, - }, + ...$ctx?.headers + } } const valueMap = new Map(Object.entries(ctx.value ?? {})) @@ -109,18 +109,18 @@ export function useFetch( const isRequest = init instanceof Object && init?.json const optionsConfig = - typeof init === "string" + typeof init === 'string' ? { // Pass init as the url if init is a string url: init, - ...options, + ...options } : isRequest ? { url: init.url, method: init.method, init, - ...options, + ...options } : (init as FetchConfigType) @@ -129,14 +129,14 @@ export function useFetch( onOffline = ctx.onOffline, onMutate, revalidateOnMount = ctx.revalidateOnMount, - url = "", + url = '', query = {}, params = {}, baseUrl = undefined, method = isRequest ? init.method : (METHODS.GET as HTTP_METHODS), headers = {} as Headers, body = undefined as unknown as Body, - formatBody = (e) => JSON.stringify(e), + formatBody = e => JSON.stringify(e), resolver = isFunction(ctx.resolver) ? ctx.resolver : DEFAULT_RESOLVER, onError, auto = isDefined(ctx.auto) ? ctx.auto : true, @@ -155,6 +155,7 @@ export function useFetch( maxCacheAge = ctx.maxCacheAge, fetcher = ctx.fetcher, middleware = ctx.middleware, + transform = ctx.transform } = optionsConfig const $fetch = isFunction(fetcher) @@ -170,7 +171,7 @@ export function useFetch( method, headers, body, - formatBody, + formatBody } const { cacheProvider: $cacheProvider = defaultCache } = ctx @@ -180,7 +181,7 @@ export function useFetch( const { cacheProvider = $cacheProvider } = optionsConfig - const requestCallId = useMemo(() => `${Math.random()}`.split(".")[1], []) + const requestCallId = useMemo(() => `${Math.random()}`.split('.')[1], []) const willResolve = isDefined(onResolve) const handleError = isDefined(onError) @@ -198,24 +199,24 @@ export function useFetch( const reqQuery = { ...ctx.query, - ...config.query, + ...config.query } const reqParams = { ...ctx.params, - ...config.params, + ...config.params } const rawUrl = (hasBaseUrl(url) - ? "" + ? '' : !isDefined(config.baseUrl) ? !isDefined(ctx.baseUrl) - ? "" + ? '' : ctx.baseUrl : config.baseUrl) + url - const defaultId = [method, url].join(" ") + const defaultId = [method, url].join(' ') const { id = defaultId } = optionsConfig @@ -234,22 +235,22 @@ export function useFetch( const resolvedDataKey = serialize({ idString, reqQuery, reqParams }) - const ageKey = ["max-age", resolvedDataKey].join("-") + const ageKey = ['max-age', resolvedDataKey].join('-') const paginationCache = cacheProvider.get(resolvedDataKey) const normalCache = cacheProvider.get(resolvedKey) - const maxAge = getMiliseconds(maxCacheAge || "0 ms") + const maxAge = getMiliseconds(maxCacheAge || '0 ms') // Revalidates if passed maxCacheAge has changed - if (!cacheProvider.get("maxAgeValue" + resolvedDataKey)) { - cacheProvider.set("maxAgeValue" + resolvedDataKey, maxCacheAge || "0 ms") + if (!cacheProvider.get('maxAgeValue' + resolvedDataKey)) { + cacheProvider.set('maxAgeValue' + resolvedDataKey, maxCacheAge || '0 ms') } else { - if (cacheProvider.get("maxAgeValue" + resolvedDataKey) !== maxCacheAge) { + if (cacheProvider.get('maxAgeValue' + resolvedDataKey) !== maxCacheAge) { cacheProvider.set(ageKey, 0) - cacheProvider.set("maxAgeValue" + resolvedDataKey, maxCacheAge) + cacheProvider.set('maxAgeValue' + resolvedDataKey, maxCacheAge) } } @@ -271,7 +272,7 @@ export function useFetch( const suspense = $suspense || willSuspend.get(resolvedKey) if (!suspense) { - if (url !== "") { + if (url !== '') { suspenseInitialized.set(resolvedKey, true) } } @@ -284,17 +285,17 @@ export function useFetch( const realUrl = urlWithParams + - (urlWithParams.includes("?") ? (optionsConfig?.query ? `&` : "") : "?") + (urlWithParams.includes('?') ? (optionsConfig?.query ? `&` : '') : '?') if (!previousProps.has(resolvedKey)) { - if (url !== "") { + if (url !== '') { previousProps.set(resolvedKey, optionsConfig) } } const configUrl = urls[resolvedKey] || { realUrl, - rawUrl, + rawUrl } const stringDeps = serialize( @@ -316,7 +317,7 @@ export function useFetch( useEffect(() => { if (isDefined(optionsConfig.default)) { if (!fetcherDefaults.has(resolvedKey)) { - if (url !== "") { + if (url !== '') { if (!isDefined(cacheProvider.get(resolvedDataKey))) { fetcherDefaults.set(resolvedKey, optionsConfig.default) } @@ -324,7 +325,7 @@ export function useFetch( if (!isDefined(cacheProvider.get(resolvedDataKey))) { requestsProvider.emit(resolvedKey, { requestCallId, - data: optionsConfig.default, + data: optionsConfig.default }) } } @@ -362,16 +363,16 @@ export function useFetch( ? isPending(resolvedKey) || (revalidateOnMount ? !jsonCompare( - JSON.parse(previousConfig.get(resolvedKey) || "{}"), + JSON.parse(previousConfig.get(resolvedKey) || '{}'), optionsConfig ) : !jsonCompare( - JSON.parse(previousConfig.get(resolvedKey) || "{}"), + JSON.parse(previousConfig.get(resolvedKey) || '{}'), optionsConfig )) : false, error: (hasErrors.get(resolvedDataKey) || false) as boolean, - completedAttempts: 0, + completedAttempts: 0 }) const thisDeps = useRef({ @@ -379,7 +380,7 @@ export function useFetch( online: false, loading: false, error: false, - completedAttempts: false, + completedAttempts: false }).current const inDeps = (k: keyof typeof thisDeps) => { @@ -400,20 +401,20 @@ export function useFetch( } function setData(v: any) { - setFetchState((p) => { + setFetchState(p => { if (isFunction(v)) { const newVal = v(p.data) if (!jsonCompare(p.data, newVal)) { return { ...p, - data: newVal, + data: newVal } } } else { if (!jsonCompare(p.data, v)) { return { ...p, - data: v, + data: v } } } @@ -426,11 +427,11 @@ export function useFetch( const rawJSON = serialize(data) function setOnline(v: any) { - setFetchState((p) => { + setFetchState(p => { if (online !== p.online) { return { ...p, - online: v, + online: v } } return p @@ -439,24 +440,24 @@ export function useFetch( const requestHeaders = { ...ctx.headers, - ...config.headers, + ...config.headers } function setError(v: any) { - setFetchState((p) => { + setFetchState(p => { if (isFunction(v)) { const newErroValue = v(p.error) if (newErroValue !== p.error) { return { ...p, - error: newErroValue, + error: newErroValue } } } else { if (v !== p.error) { return { ...p, - error: v, + error: v } } } @@ -465,20 +466,20 @@ export function useFetch( } function setLoading(v: any) { - setFetchState((p) => { + setFetchState(p => { if (isFunction(v)) { const newLoadingValue = v(p.loading) if (newLoadingValue !== p.loading) { return { ...p, - loading: newLoadingValue, + loading: newLoadingValue } } } else { if (v !== p.loading) { return { ...p, - loading: v, + loading: v } } } @@ -487,20 +488,20 @@ export function useFetch( } function setCompletedAttempts(v: any) { - setFetchState((p) => { + setFetchState(p => { if (isFunction(v)) { const newCompletedAttempts = v(p.completedAttempts) if (newCompletedAttempts !== p.completedAttempts) { return { ...p, - completedAttempts: newCompletedAttempts, + completedAttempts: newCompletedAttempts } } } else { if (v !== p.completedAttempts) { return { ...p, - completedAttempts: v, + completedAttempts: v } } } @@ -511,7 +512,7 @@ export function useFetch( const requestAbortController: AbortController = abortControllers.get(resolvedKey) ?? new AbortController() - const isGqlRequest = isDefined((optionsConfig as any)["__gql"]) + const isGqlRequest = isDefined((optionsConfig as any)['__gql']) const fetchData = useCallback( async function fetchData( @@ -519,10 +520,10 @@ export function useFetch( ) { const rawUrl = (hasBaseUrl(url) - ? "" + ? '' : !isDefined(config.baseUrl) ? !isDefined(ctx.baseUrl) - ? "" + ? '' : ctx.baseUrl : config.baseUrl) + url @@ -530,20 +531,20 @@ export function useFetch( const realUrl = urlWithParams + - (urlWithParams.includes("?") ? (c?.query !== "" ? `&` : "") : "") + (urlWithParams.includes('?') ? (c?.query !== '' ? `&` : '') : '') if ( !jsonCompare( - JSON.parse(previousConfig.get(resolvedKey) || "{}"), + JSON.parse(previousConfig.get(resolvedKey) || '{}'), optionsConfig ) ) { previousProps.set(resolvedKey, optionsConfig) queue(() => { - if (url !== "") { + if (url !== '') { const newUrls = { realUrl, - rawUrl, + rawUrl } urls[resolvedKey] = newUrls @@ -567,7 +568,7 @@ export function useFetch( requestCallId: loadingFirst ? requestCallId : undefined, loading: true, requestAbortController: newAbortController, - error: false, + error: false }) abortControllers.set(resolvedKey, newAbortController) @@ -589,7 +590,7 @@ export function useFetch( } } - cacheProvider.set("requestStart" + resolvedDataKey, Date.now()) + cacheProvider.set('requestStart' + resolvedDataKey, Date.now()) requestInitialTimes.set(resolvedDataKey, Date.now()) const newRequestConfig = ( @@ -602,12 +603,12 @@ export function useFetch( return newAbortController.signal })(), headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', ...ctx.headers, ..._headers, ...config.headers, - ...c.headers, - }, + ...c.headers + } } : { ...ctx, @@ -624,23 +625,23 @@ export function useFetch( headers: { ...(temporaryFormData.get(resolvedKey) ? {} - : { "Content-Type": "application/json" }), + : { 'Content-Type': 'application/json' }), ...ctx.headers, ...config.headers, - ...c.headers, - }, + ...c.headers + } } ) as any const r = new Request( ( realUrl + - (realUrl.includes("?") + (realUrl.includes('?') ? c.query : c.query - ? "?" + c.query + ? '?' + c.query : c.query) - ).replace("?&", "?"), + ).replace('?&', '?'), newRequestConfig ) @@ -654,11 +655,11 @@ export function useFetch( const resolvedDate = Date.now() cacheProvider.set( - "expiration" + resolvedDataKey, + 'expiration' + resolvedDataKey, resolvedDate + maxAge ) - cacheProvider.set("requestEnds" + resolvedDataKey, resolvedDate) + cacheProvider.set('requestEnds' + resolvedDataKey, resolvedDate) requestResponseTimes.set( resolvedDataKey, getTimePassed(resolvedDataKey) @@ -673,14 +674,14 @@ export function useFetch( ...rpc, response: json, error: false, - code, + code } // @ts-ignore - 'data' is priority because 'fetcher' can return it - const incoming = json?.["data"] ?? (await (resolver as any)(json)) + const incoming = json?.['data'] ?? (await (resolver as any)(json)) - // @ts-expect-error - const actionError = json?.["error"] + // @ts-ignore + const actionError = json?.['error'] const _data = isFunction(middleware) ? await middleware!(incoming as any, thisCache) @@ -690,7 +691,7 @@ export function useFetch( ? { ..._data, variables: (optionsConfig as any)?.variables, - errors: _data?.errors ? _data.errors : undefined, + errors: _data?.errors ? _data.errors : undefined } : _data @@ -708,7 +709,7 @@ export function useFetch( rpc = { ...rpc, - error: false, + error: false } const dataExpirationTime = Date.now() + maxAge @@ -719,7 +720,7 @@ export function useFetch( hasErrors.set(resolvedKey, true) rpc = { ...rpc, - error: true, + error: true } if (handleError) { if (!resolvedOnErrorCalls.get(resolvedKey)) { @@ -737,7 +738,7 @@ export function useFetch( variables: isGqlRequest ? (optionsConfig as any)?.variables || {} : undefined, - completedAttempts: 0, + completedAttempts: 0 } $$data = __data @@ -746,7 +747,7 @@ export function useFetch( if (!_data?.errors && isGqlRequest) { rpc = { ...rpc, - error: false, + error: false } hasErrors.set(resolvedDataKey, false) @@ -769,7 +770,7 @@ export function useFetch( } else { rpc = { ...rpc, - error: actionError ?? true, + error: actionError ?? true } if (!cacheIfError) { hasData.set(resolvedDataKey, false) @@ -780,11 +781,11 @@ export function useFetch( hasData.set(resolvedDataKey, false) hasData.set(resolvedKey, false) } - setFetchState((previous) => { + setFetchState(previous => { const newData = { ...previous, variables: (optionsConfig as any)?.variables, - errors: _data.errors, + errors: _data.errors } as any $$data = newData @@ -794,7 +795,7 @@ export function useFetch( rpc = { ...rpc, data: newData, - error: actionError ?? true, + error: actionError ?? true } cacheProvider.set(resolvedDataKey, newData) @@ -815,7 +816,7 @@ export function useFetch( rpc = { ...rpc, - data: def, + data: def } } if (handleError) { @@ -849,7 +850,7 @@ export function useFetch( rpc = { ...rpc, - error: err ?? true, + error: err ?? true } if (cacheIfError) { @@ -859,7 +860,7 @@ export function useFetch( rpc = { ...rpc, - data: thisCache, + data: thisCache } } } else { @@ -867,7 +868,7 @@ export function useFetch( rpc = { ...rpc, - data: def, + data: def } cacheForMutation.set(idString, def) @@ -875,7 +876,7 @@ export function useFetch( rpc = { ...rpc, - error: err ?? true, + error: err ?? true } hasErrors.set(resolvedDataKey, err ?? true) @@ -889,7 +890,7 @@ export function useFetch( } else { rpc = { ...rpc, - loading: true, + loading: true } if (!isPending(resolvedKey)) { if (!isDefined(cacheProvider.get(resolvedDataKey))) { @@ -900,7 +901,7 @@ export function useFetch( rpc = { ...rpc, data: def, - loading: true, + loading: true } } } @@ -919,7 +920,7 @@ export function useFetch( hasErrors.get(resolvedDataKey) || false, ...rpc, - loading: false, + loading: false }) willSuspend.set(resolvedKey, false) @@ -944,7 +945,7 @@ export function useFetch( requestCallId, memory, def, - loadingFirst, + loadingFirst ] ) @@ -960,21 +961,21 @@ export function useFetch( } } } - signal?.addEventListener("abort", abortCallback) + signal?.addEventListener('abort', abortCallback) return () => { - signal?.removeEventListener("abort", abortCallback) + signal?.removeEventListener('abort', abortCallback) } }, [requestAbortController, resolvedKey, onAbort, loading]) const imperativeFetch = useMemo(() => { const __headers = { ...ctx.headers, - ...config.headers, + ...config.headers } const __params = { ...ctx.params, - ...config.params, + ...config.params } const __baseUrl = isDefined(config.baseUrl) ? config.baseUrl : ctx.baseUrl @@ -982,7 +983,7 @@ export function useFetch( ...ctx, headers: __headers, baseUrl: __baseUrl, - params: __params, + params: __params }) }, [serialize(ctx)]) @@ -994,7 +995,7 @@ export function useFetch( error: $error, online, loading, - completedAttempts, + completedAttempts } = v || {} if (isMutating) { @@ -1003,7 +1004,7 @@ export function useFetch( if (isMutating) { forceMutate($data) if (handleMutate) { - if (url === "") { + if (url === '') { ;(onMutate as any)($data, imperativeFetch) } else { if (!runningMutate.get(resolvedKey)) { @@ -1019,31 +1020,31 @@ export function useFetch( if (v.requestCallId !== requestCallId) { if (!willSuspend.get(resolvedKey)) { queue(() => { - if (inDeps("data")) { + if (inDeps('data')) { if (isDefined($data)) { if (!jsonCompare(data, cacheProvider.get(resolvedDataKey))) { setData(cacheProvider.get(resolvedKey)) } } } - if (inDeps("online")) { + if (inDeps('online')) { if (isDefined(online)) { setOnline(online) } } - if (inDeps("loading")) { + if (inDeps('loading')) { if (isDefined(loading)) { setLoading(loading) } } - if (inDeps("error")) { + if (inDeps('error')) { if (isDefined($error)) { if (fetchState.error !== $error) { setError($error) } } } - if (inDeps("completedAttempts")) { + if (inDeps('completedAttempts')) { if (isDefined(completedAttempts)) { setCompletedAttempts(completedAttempts) } @@ -1064,7 +1065,7 @@ export function useFetch( resolvedKey, resolvedDataKey, requestCallId, - fetchState, + fetchState ]) const reValidate = useCallback( @@ -1081,18 +1082,18 @@ export function useFetch( if (!isPending(resolvedKey)) { // preventing revalidation where only need updates about // 'loading', 'error' and 'data' because the url can be ommited. - if (url !== "") { + if (url !== '') { fetchData({ query: Object.keys(reqQuery) - .map((q) => + .map(q => Array.isArray(reqQuery[q]) ? reqQuery[q] - .map((queryItem: any) => [q, queryItem].join("=")) - .join("&") - : [q, reqQuery[q]].join("=") + .map((queryItem: any) => [q, queryItem].join('=')) + .join('&') + : [q, reqQuery[q]].join('=') ) - .join("&"), - params: reqParams, + .join('&'), + params: reqParams }) } } @@ -1113,7 +1114,7 @@ export function useFetch( ctx.auto, idString, fetchState, - id, + id ]) useEffect(() => { @@ -1124,7 +1125,7 @@ export function useFetch( } requestsProvider.emit(resolvedKey, { requestCallId, - online: true, + online: true }) setOnline(true) offlineHandled.set(resolvedKey, false) @@ -1141,9 +1142,9 @@ export function useFetch( function addOnlineListener() { if (windowExists) { - if ("addEventListener" in window) { + if ('addEventListener' in window) { if (retryOnReconnect) { - window.addEventListener("online", backOnline) + window.addEventListener('online', backOnline) } } } @@ -1153,8 +1154,8 @@ export function useFetch( return () => { if (windowExists) { - if ("addEventListener" in window) { - window.removeEventListener("online", backOnline) + if ('addEventListener' in window) { + window.removeEventListener('online', backOnline) } } } @@ -1166,7 +1167,7 @@ export function useFetch( setOnline(false) requestsProvider.emit(resolvedKey, { requestCallId, - online: false, + online: false }) onlineHandled.set(resolvedKey, false) if (handleOffline) { @@ -1179,8 +1180,8 @@ export function useFetch( function addOfflineListener() { if (windowExists) { - if ("addEventListener" in window) { - window.addEventListener("offline", wentOffline) + if ('addEventListener' in window) { + window.addEventListener('offline', wentOffline) } } } @@ -1189,8 +1190,8 @@ export function useFetch( return () => { if (windowExists) { - if ("addEventListener" in window) { - window.removeEventListener("offline", wentOffline) + if ('addEventListener' in window) { + window.removeEventListener('offline', wentOffline) } } } @@ -1200,7 +1201,7 @@ export function useFetch( return () => { if (revalidateOnMount) { if (canRevalidate) { - if (url !== "") { + if (url !== '') { if (suspenseInitialized.get(resolvedKey)) { queue(() => { previousConfig.set(resolvedKey, undefined) @@ -1233,7 +1234,7 @@ export function useFetch( if (!gettingAttempts.get(resolvedKey)) { gettingAttempts.set(resolvedKey, true) const attempts = - typeof $attempts === "function" + typeof $attempts === 'function' ? $attempts({ status: statusCodes.get(resolvedKey) || @@ -1243,7 +1244,7 @@ export function useFetch( hasErrors.get(resolvedKey) || hasErrors.get(resolvedDataKey) || (error as any), - completedAttempts, + completedAttempts }) : $attempts @@ -1255,7 +1256,7 @@ export function useFetch( requestsProvider.emit(resolvedKey, { requestCallId, - completedAttempts: newAttemptsValue, + completedAttempts: newAttemptsValue }) return newAttemptsValue @@ -1264,9 +1265,9 @@ export function useFetch( requestsProvider.emit(resolvedKey, { requestCallId, online: false, - error: true, + error: true }) - if (inDeps("online")) setOnline(false) + if (inDeps('online')) setOnline(false) } } } @@ -1284,7 +1285,7 @@ export function useFetch( fetchState, attemptInterval, resolvedKey, - completedAttempts, + completedAttempts ]) useEffect(() => { @@ -1307,7 +1308,7 @@ export function useFetch( rawJSON, canRevalidate, completedAttempts, - config, + config ]) const initializeRevalidation = useCallback( @@ -1315,28 +1316,28 @@ export function useFetch( ? async function initializeRevalidation() { let d = undefined if (canRevalidate) { - if (url !== "") { + if (url !== '') { d = await fetchData({ query: Object.keys(reqQuery) - .map((q) => + .map(q => Array.isArray(reqQuery[q]) ? reqQuery[q] - .map((queryItem: any) => [q, queryItem].join("=")) - .join("&") - : [q, reqQuery[q]].join("=") + .map((queryItem: any) => [q, queryItem].join('=')) + .join('&') + : [q, reqQuery[q]].join('=') ) - .join("&"), - params: reqParams, + .join('&'), + params: reqParams }) } else { d = def // It means a url is not passed - setFetchState((prev) => ({ + setFetchState(prev => ({ ...prev, loading: false, error: hasErrors.get(resolvedDataKey) || hasErrors.get(resolvedKey), - completedAttempts: prev.completedAttempts, + completedAttempts: prev.completedAttempts })) } } else { @@ -1353,17 +1354,17 @@ export function useFetch( ) if (!suspense) { - if (url !== "") { + if (url !== '') { suspenseInitialized.set(resolvedKey, true) } } useIsomorphicLayoutEffect(() => { const fn = () => { - if (url !== "") { + if (url !== '') { if (!jsonCompare(previousProps.get(resolvedKey), optionsConfig)) { abortControllers.get(resolvedKey)?.abort() - if (inDeps("data")) { + if (inDeps('data')) { queue(initializeRevalidation) } } @@ -1412,15 +1413,15 @@ Learn more: https://httpr.vercel.app/docs/api#suspense const fn = () => { if (!runningRequests.get(resolvedKey) && canRevalidate) { if (windowExists) { - if (canRevalidate && url !== "") { + if (canRevalidate && url !== '') { if ( !jsonCompare( - JSON.parse(previousConfig.get(resolvedKey) || "{}"), + JSON.parse(previousConfig.get(resolvedKey) || '{}'), optionsConfig ) ) { if (!isPending(resolvedKey)) { - if (inDeps("data")) { + if (inDeps('data')) { initializeRevalidation() } } else { @@ -1445,13 +1446,13 @@ Learn more: https://httpr.vercel.app/docs/api#suspense const revalidateAfterUnmount = revalidateOnMount ? true : !jsonCompare( - JSON.parse(previousConfig.get(resolvedKey) || "{}"), + JSON.parse(previousConfig.get(resolvedKey) || '{}'), optionsConfig ) function revalidate() { if (!debounce && !canDebounce.get(resolvedKey)) { - if (inDeps("data")) { + if (inDeps('data')) { initializeRevalidation() } } @@ -1483,8 +1484,8 @@ Learn more: https://httpr.vercel.app/docs/api#suspense useEffect(() => { function addFocusListener() { if (revalidateOnFocus && windowExists) { - if ("addEventListener" in window) { - window.addEventListener("focus", reValidate as any) + if ('addEventListener' in window) { + window.addEventListener('focus', reValidate as any) } } } @@ -1493,8 +1494,8 @@ Learn more: https://httpr.vercel.app/docs/api#suspense return () => { if (windowExists) { - if ("addEventListener" in window) { - window.removeEventListener("focus", reValidate as any) + if ('addEventListener' in window) { + window.removeEventListener('focus', reValidate as any) } } } @@ -1506,7 +1507,7 @@ Learn more: https://httpr.vercel.app/docs/api#suspense loading, reValidate, refresh, - serialize(config), + serialize(config) ]) const __config = { @@ -1515,20 +1516,20 @@ Learn more: https://httpr.vercel.app/docs/api#suspense ...previousProps.get(resolvedKey), params: { ...reqParams, - ...previousProps.get(resolvedKey)?.params, + ...previousProps.get(resolvedKey)?.params }, headers: { ...requestHeaders, - ...previousProps.get(resolvedKey)?.headers, + ...previousProps.get(resolvedKey)?.headers }, body: config.body, baseUrl: ctx.baseUrl || config.baseUrl, - url: configUrl?.realUrl?.replace("?", ""), + url: configUrl?.realUrl?.replace('?', ''), rawUrl: configUrl?.rawUrl, query: { ...reqQuery, - ...previousProps.get(resolvedKey)?.query, - }, + ...previousProps.get(resolvedKey)?.query + } } function forceMutate( @@ -1548,7 +1549,7 @@ Learn more: https://httpr.vercel.app/docs/api#suspense requestsProvider.emit(resolvedKey, { requestCallId, isMutating: true, - data: newValue, + data: newValue }) setData(newValue as any) } @@ -1564,7 +1565,7 @@ Learn more: https://httpr.vercel.app/docs/api#suspense requestsProvider.emit(resolvedKey, { requestCallId, isMutating: true, - data: newVal, + data: newVal }) setData(newVal) @@ -1573,12 +1574,12 @@ Learn more: https://httpr.vercel.app/docs/api#suspense } const [$requestStart, $requestEnd] = [ - notNull(cacheProvider.get("requestStart" + resolvedDataKey)) - ? new Date(cacheProvider.get("requestStart" + resolvedDataKey)) - : null, - notNull(cacheProvider.get("requestEnds" + resolvedDataKey)) - ? new Date(cacheProvider.get("requestEnds" + resolvedDataKey)) + notNull(cacheProvider.get('requestStart' + resolvedDataKey)) + ? new Date(cacheProvider.get('requestStart' + resolvedDataKey)) : null, + notNull(cacheProvider.get('requestEnds' + resolvedDataKey)) + ? new Date(cacheProvider.get('requestEnds' + resolvedDataKey)) + : null ] const expirationDate = error @@ -1587,16 +1588,20 @@ Learn more: https://httpr.vercel.app/docs/api#suspense : null : maxAge === 0 ? null - : notNull(cacheProvider.get("expiration" + resolvedDataKey)) - ? new Date(cacheProvider.get("expiration" + resolvedDataKey)) + : notNull(cacheProvider.get('expiration' + resolvedDataKey)) + ? new Date(cacheProvider.get('expiration' + resolvedDataKey)) : null const isFailed = hasErrors.get(resolvedDataKey) || hasErrors.get(resolvedKey) || error - const responseData = + const dataCandidate = (error && isFailed ? (cacheIfError ? thisCache : null) : thisCache) ?? def + const responseData = isDefined(dataCandidate) + ? transform!(dataCandidate) + : dataCandidate + const isSuccess = !isLoading && !isFailed const oneRequestResolved = @@ -1610,7 +1615,7 @@ Learn more: https://httpr.vercel.app/docs/api#suspense if (isFormData(form)) { if (formRef.current) { if (onSubmit) { - if (onSubmit !== "reset") { + if (onSubmit !== 'reset') { onSubmit(formRef.current, form) } else { if (formRef.current) { @@ -1634,7 +1639,7 @@ Learn more: https://httpr.vercel.app/docs/api#suspense hasErrors.set(resolvedDataKey, false) requestsProvider.emit(resolvedKey, { - error: false, + error: false }) } @@ -1649,7 +1654,7 @@ Learn more: https://httpr.vercel.app/docs/api#suspense }, formProps: { action: submit, - ref: formRef, + ref: formRef }, formRef, get revalidating() { @@ -1746,7 +1751,7 @@ Learn more: https://httpr.vercel.app/docs/api#suspense requestCallId, error: false, loading: false, - data: requestCache, + data: requestCache }) } } @@ -1760,7 +1765,7 @@ Learn more: https://httpr.vercel.app/docs/api#suspense /** * The request key */ - key: resolvedKey, + key: resolvedKey } as unknown as { refresh(): void resetError(): void @@ -1808,15 +1813,15 @@ Learn more: https://httpr.vercel.app/docs/api#suspense } } -useFetch.get = createRequestFn("GET", "", {}) -useFetch.delete = createRequestFn("DELETE", "", {}) -useFetch.head = createRequestFn("HEAD", "", {}) -useFetch.options = createRequestFn("OPTIONS", "", {}) -useFetch.post = createRequestFn("POST", "", {}) -useFetch.put = createRequestFn("PUT", "", {}) -useFetch.patch = createRequestFn("PATCH", "", {}) -useFetch.purge = createRequestFn("PURGE", "", {}) -useFetch.link = createRequestFn("LINK", "", {}) -useFetch.unlink = createRequestFn("UNLINK", "", {}) +useFetch.get = createRequestFn('GET', '', {}) +useFetch.delete = createRequestFn('DELETE', '', {}) +useFetch.head = createRequestFn('HEAD', '', {}) +useFetch.options = createRequestFn('OPTIONS', '', {}) +useFetch.post = createRequestFn('POST', '', {}) +useFetch.put = createRequestFn('PUT', '', {}) +useFetch.patch = createRequestFn('PATCH', '', {}) +useFetch.purge = createRequestFn('PURGE', '', {}) +useFetch.link = createRequestFn('LINK', '', {}) +useFetch.unlink = createRequestFn('UNLINK', '', {}) useFetch.extend = createImperativeFetch diff --git a/src/internal/constants.ts b/src/internal/constants.ts index 5800cba..ef003dd 100644 --- a/src/internal/constants.ts +++ b/src/internal/constants.ts @@ -9,21 +9,22 @@ const ATTEMPT_INTERVAL = 2 const REVALIDATE_ON_FOCUS = false const RETRY_ON_RECONNECT = true const REVALIDATE_ON_MOUNT = true -const DEFAULT_GRAPHQL_PATH = '/graphql' +const DEFAULT_GRAPHQL_PATH = "/graphql" const DEFAULT_RESOLVER = (e: any) => e.json() const DEFAULT_MIDDLEWARE = (incoming: any, previous: any) => incoming +const DEFAULT_TRANSFORM = (fetchData: any) => fetchData const METHODS = { - GET: 'GET', - DELETE: 'DELETE', - HEAD: 'HEAD', - OPTIONS: 'OPTIONS', - POST: 'POST', - PUT: 'PUT', - PATCH: 'PATCH', - PURGE: 'PURGE', - LINK: 'LINK', - UNLINK: 'UNLINK' + GET: "GET", + DELETE: "DELETE", + HEAD: "HEAD", + OPTIONS: "OPTIONS", + POST: "POST", + PUT: "PUT", + PATCH: "PATCH", + PURGE: "PURGE", + LINK: "LINK", + UNLINK: "UNLINK", } const UNITS_MILISECONDS_EQUIVALENTS = { @@ -34,7 +35,7 @@ const UNITS_MILISECONDS_EQUIVALENTS = { d: 86400000, we: 604800000, mo: 2629800000, - y: 31536000000 + y: 31536000000, } export { @@ -53,5 +54,6 @@ export { DEFAULT_RESOLVER, METHODS, UNITS_MILISECONDS_EQUIVALENTS, - DEFAULT_MIDDLEWARE + DEFAULT_MIDDLEWARE, + DEFAULT_TRANSFORM, } diff --git a/src/internal/index.ts b/src/internal/index.ts index 9109aba..29b3487 100644 --- a/src/internal/index.ts +++ b/src/internal/index.ts @@ -1,12 +1,13 @@ -'use client' -import { createContext, useContext } from 'react' +"use client" +import { createContext, useContext } from "react" -import { CacheStoreType, FetchContextType } from '../types' +import { CacheStoreType, FetchContextType } from "../types" import { ATTEMPTS, ATTEMPT_INTERVAL, DEFAULTS, DEFAULT_MIDDLEWARE, + DEFAULT_TRANSFORM, ONLINE, ON_OFFLINE, ON_ONLINE, @@ -14,9 +15,9 @@ import { QUERY, RETRY_ON_RECONNECT, REVALIDATE_ON_FOCUS, - REVALIDATE_ON_MOUNT -} from './constants' -import { $context } from './shared' + REVALIDATE_ON_MOUNT, +} from "./constants" +import { $context } from "./shared" /** * This marks which requests are running @@ -134,7 +135,7 @@ export const defaultCache: CacheStoreType = { }, remove(k) { resolvedRequests.delete(k) - } + }, } const requestsSubscribers = new Map() @@ -162,7 +163,7 @@ export const requestsProvider = { listener(payload) }) } - } + }, } const defaultContextVaue: FetchContextType = { @@ -179,7 +180,8 @@ const defaultContextVaue: FetchContextType = { revalidateOnMount: REVALIDATE_ON_MOUNT, cacheIfError: true, middleware: DEFAULT_MIDDLEWARE, - ...$context.value + transform: DEFAULT_TRANSFORM, + ...$context.value, } export const FetchContext = createContext(defaultContextVaue) diff --git a/src/types/index.ts b/src/types/index.ts index 92a68d1..0d3af63 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,14 +1,14 @@ export type HTTP_METHODS = - | 'GET' - | 'DELETE' - | 'HEAD' - | 'OPTIONS' - | 'POST' - | 'PUT' - | 'PATCH' - | 'PURGE' - | 'LINK' - | 'UNLINK' + | "GET" + | "DELETE" + | "HEAD" + | "OPTIONS" + | "POST" + | "PUT" + | "PATCH" + | "PURGE" + | "LINK" + | "UNLINK" export type FetchContextType = { clientOnly?: boolean @@ -55,6 +55,7 @@ export type FetchContextType = { suspense?: any[] resolver?: (r: Response) => any middleware?(incomindgData: any, previousData: any): any + transform?(fetchData: any): any children?: any auto?: boolean memory?: boolean @@ -82,7 +83,7 @@ export type FetchContextType = { ctx: FetchContextType ): void maxCacheAge?: TimeSpan -} & Omit +} & Omit export type CacheStoreType = { get(k?: any): any @@ -90,7 +91,7 @@ export type CacheStoreType = { remove?(k?: any): any } -export type CustomResponse = Omit & { +export type CustomResponse = Omit & { json(): Promise } @@ -102,7 +103,7 @@ export type RequestWithBody = ( /** * The request configuration */ - reqConfig?: Omit, 'suspense'> & { + reqConfig?: Omit, "suspense"> & { /** * Default value */ @@ -143,7 +144,7 @@ export type RequestWithBody = ( export type TimeSpan = | number - | `${string} ${'ms' | 'sec' | 'min' | 'h' | 'd' | 'we' | 'mo' | 'y'}` + | `${string} ${"ms" | "sec" | "min" | "h" | "d" | "we" | "mo" | "y"}` /** * An imperative version of the `useFetch` hook @@ -164,7 +165,7 @@ export type ImperativeFetch = { export type FetchConfigType = Omit< RequestInit, - 'body' | 'headers' + "body" | "headers" > & { headers?: any /** @@ -176,6 +177,7 @@ export type FetchConfigType = Omit< incomindgData: FetchDataType, previousData: FetchDataType ): FetchDataType + transform?(fetchData: FetchDataType): FetchDataType fetcher?( url: string, config: FetchConfigType @@ -217,7 +219,7 @@ export type FetchConfigType = Omit< * @default true */ memory?: boolean - onSubmit?: 'reset' | ((form: HTMLFormElement, data: FormData) => void) + onSubmit?: "reset" | ((form: HTMLFormElement, data: FormData) => void) /** * Function to run when request is resolved succesfuly */ @@ -335,11 +337,11 @@ export type FetchConfigType = Omit< /** * Will run when the request is sent */ - onFetchStart?: FetchContextType['onFetchStart'] + onFetchStart?: FetchContextType["onFetchStart"] /** * Will run when the response is received */ - onFetchEnd?: FetchContextType['onFetchEnd'] + onFetchEnd?: FetchContextType["onFetchEnd"] /** * If `true`, the last resolved value be returned as `data` if the request fails. If `false`, the default value will be returned instead * @@ -355,7 +357,7 @@ export type FetchConfigType = Omit< // If first argument is a string export type FetchConfigTypeNoUrl = Omit< FetchConfigType, - 'url' + "url" > /**