UNPKG

@supunlakmal/hooks

Version:

A collection of reusable React hooks

122 lines 4.88 kB
import { useState, useEffect, useRef, useCallback } from 'react'; // In-memory cache store (shared across hook instances if defined outside) const globalCache = new Map(); const DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes /** * Fetches data like `useFetch` but includes a simple in-memory cache * to avoid redundant requests for the same URL within a configurable TTL. * * @template T The expected type of the data to be fetched. * @param url The URL to fetch data from. * @param options Configuration options including TTL, fetch options, and cache key generation. * @returns State object with data, error, status, and refetch function. */ export const useCachedFetch = (url, options = {}) => { const { ttl = DEFAULT_TTL, fetchOptions, getCacheKey = (u) => u, // Default key is just the URL } = options; const cacheKey = url ? getCacheKey(url, fetchOptions) : ''; // Generate cache key // State for the fetch status const [status, setStatus] = useState('idle'); const [data, setData] = useState(() => { // Initialize state from cache if available and valid const cachedEntry = globalCache.get(cacheKey); if (cachedEntry && Date.now() - cachedEntry.timestamp <= ttl) { return cachedEntry.data; } return undefined; }); const [error, setError] = useState(undefined); // Ref to track mounted status and prevent state updates after unmount const isMounted = useRef(true); useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); // Ref to store options without causing effect re-runs const optionsRef = useRef(options); useEffect(() => { optionsRef.current = options; }, [options]); const fetchData = useCallback(async (ignoreCache = false) => { var _a, _b; if (!url) { if (isMounted.current) { setStatus('idle'); setData(undefined); setError(undefined); } return; } const currentCacheKey = getCacheKey(url, optionsRef.current.fetchOptions); const currentTtl = (_a = optionsRef.current.ttl) !== null && _a !== void 0 ? _a : DEFAULT_TTL; const currentCacheOnlyIfFresh = (_b = optionsRef.current.cacheOnlyIfFresh) !== null && _b !== void 0 ? _b : false; if (!ignoreCache) { const cachedEntry = globalCache.get(currentCacheKey); const isFresh = cachedEntry && Date.now() - cachedEntry.timestamp <= currentTtl; if (cachedEntry && (isFresh || !currentCacheOnlyIfFresh)) { if (isMounted.current) { setData(cachedEntry.data); setStatus('success'); setError(undefined); } return; // Serve from cache } } if (isMounted.current) { setStatus('loading'); setError(undefined); // Clear previous error on new fetch } try { const response = await fetch(url, optionsRef.current.fetchOptions); if (!response.ok) { let errorPayload; try { errorPayload = await response.json(); // Try to parse error body } catch (_c) { errorPayload = response.statusText; // Fallback to status text } throw new Error(typeof errorPayload === 'string' ? errorPayload : JSON.stringify(errorPayload)); } const result = await response.json(); if (isMounted.current) { setData(result); setStatus('success'); setError(undefined); // Update cache globalCache.set(currentCacheKey, { data: result, timestamp: Date.now(), }); } } catch (err) { if (isMounted.current) { setError(err instanceof Error ? err : new Error(String(err))); setStatus('error'); } } }, [url, getCacheKey]); // Dependencies: url and key generation logic // Initial fetch effect useEffect(() => { fetchData(); }, [fetchData]); // Re-run if URL or getCacheKey changes const refetch = useCallback(async (opts) => { await fetchData(opts === null || opts === void 0 ? void 0 : opts.ignoreCache); }, [fetchData]); return { data, error, status, isLoading: status === 'loading', isSuccess: status === 'success', isError: status === 'error', isIdle: status === 'idle', refetch, }; }; //# sourceMappingURL=useCachedFetch.js.map