@supunlakmal/hooks
Version:
A collection of reusable React hooks
122 lines • 4.88 kB
JavaScript
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