From 30a26b685018cfee428bfe9f616027c9697c0ab7 Mon Sep 17 00:00:00 2001 From: Konstantin Lukas Date: Thu, 12 Sep 2024 17:23:49 +0200 Subject: [PATCH] add preFetchCallback to useFetch --- package.json | 2 +- src/hooks/useFetch.ts | 14 ++++++++++++-- tests/useFetch.test.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index af84db6..df26e4b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "anzol", - "version": "2.5.2", + "version": "2.6.0", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", "types": "dist/index.d.ts", diff --git a/src/hooks/useFetch.ts b/src/hooks/useFetch.ts index cad3d67..4e04bd8 100644 --- a/src/hooks/useFetch.ts +++ b/src/hooks/useFetch.ts @@ -1,3 +1,4 @@ +import type { Dispatch, SetStateAction } from "react"; import { useEffect, useState } from "react"; /** Possible return types depending on how the result is parsed. */ @@ -22,7 +23,7 @@ export interface FetchResult { status: number | undefined } -export interface FetchOptions { +export interface FetchOptions { /** How to parse the result (determines type of returned data). Set to "response" if you * don't want to extract the data automatically and receive the response object instead. */ parseType?: ParseType, @@ -37,6 +38,12 @@ export interface FetchOptions { * to re-fetch on failure. If the function returns undefined, it stops the hook from re-fetching again until an * input parameter to the hook changes and resets the internal retry counter.*/ retryTimeout?: (attempt: number) => number | undefined, + /** This callback gets called before fetching. It gives you access to the setter function for the data and the url + * to fetch. The return value of this function is a boolean indicating whether to perform a fetch from the new + * url. The intended use for this hook is mostly to prevent fetching from URLs that you know are going + * to return a non-200 result. In that case you can keep the previous state or set some empty state depending on + * your use case. You can of course also just use this hook to execute some arbitrary code before fetching. */ + preFetchCallback?: (setState: Dispatch>, newUrl: string) => boolean, } /** * Fetches the provided URL and optionally parses the response. Aborts requests when a new request is @@ -87,7 +94,8 @@ function useFetch( requestOptions = {}, discardStaleRequests = true, retryTimeout = undefined, - }: FetchOptions = {}, + preFetchCallback = undefined, + }: FetchOptions = {}, ): FetchResult { const [status, setStatus] = useState(); const [ok, setOk] = useState(false); @@ -100,6 +108,8 @@ function useFetch( }, [url, parseType, discardStaleRequests, retryTimeout]); useEffect(() => { + const performFetch = typeof preFetchCallback !== "function" || preFetchCallback(setData, url); + if (!performFetch) return; const abortController = new AbortController(); const { signal } = abortController; (async () => { diff --git a/tests/useFetch.test.ts b/tests/useFetch.test.ts index 64f04db..6ab50cf 100644 --- a/tests/useFetch.test.ts +++ b/tests/useFetch.test.ts @@ -416,4 +416,31 @@ describe("useFetch", () => { expect(retryTimeoutNoLimit).not.toHaveBeenCalledTimes(0); }); }); + + test("should allow preventing fetches via the pre fetch callback", async () => { + global.fetch = jest.fn(async () => { + await new Promise((r) => setTimeout(r, 0)); + return Promise.resolve({ + ok: false, + status: 404, + }); + }) as jest.Mock; + + const callback = jest.fn((setState) => { + setState("banana"); + return false; + }); + + const { result } = renderHook(() => useFetch("/api", { preFetchCallback: callback })); + + expect(result.current.data).toEqual("banana"); + expect(result.current.loading).toBe(false); + expect(result.current.ok).toBe(false); + expect(result.current.status).toEqual(undefined); + await waitFor(() => { + expect(callback).toHaveBeenCalledTimes(1); + }); + expect(global.fetch).not.toHaveBeenCalled(); + + }); }); \ No newline at end of file