1+ import { useState , useEffect , useRef , useMemo } from 'react' ;
2+ import axiosInstance from '../api/axiosInstance' ;
3+
4+ // In-memory cache stored outside the component lifecycle
5+ const cache = new Map ( ) ;
6+
7+ /**
8+ * Invalidates the cache. Can clear the entire cache or a specific entry.
9+ * @param {string | string[] } [urlOrUrls] - The specific URL or URLs to remove from the cache. If not provided, the entire cache is cleared.
10+ */
11+ export const invalidateCache = ( url ) => {
12+ if ( url ) {
13+ cache . delete ( url ) ;
14+ } else {
15+ cache . clear ( ) ;
16+ }
17+ } ;
18+
19+ /**
20+ * A custom hook to fetch data from a single URL or multiple URLs with in-memory caching.
21+ * It manages loading, error, and data states.
22+ * @param {string | string[] | null } urlOrUrls The URL or array of URLs to fetch data from.
23+ * @returns {{
24+ * data: any | null,
25+ * loading: boolean,
26+ * error: Error | null,
27+ * errors: Error[],
28+ * refetch: () => void
29+ * }} An object containing the fetched data, loading state, error details, and a refetch function.
30+ */
31+ export const useFetchnCache = ( urlOrUrls ) => {
32+ const [ data , setData ] = useState ( null ) ;
33+ const [ loading , setLoading ] = useState ( false ) ;
34+ const [ error , setError ] = useState ( null ) ;
35+ const [ errors , setErrors ] = useState ( [ ] ) ;
36+ const [ forceRefetch , setForceRefetch ] = useState ( 0 ) ;
37+ // Use a ref to track the current URL to prevent race conditions
38+ const requestRef = useRef ( urlOrUrls ) ;
39+ requestRef . current = urlOrUrls ;
40+
41+ // Memoize the stringified URL(s) to use as a stable dependency for useEffect.
42+ const urlsKey = useMemo ( ( ) => {
43+ return urlOrUrls ? JSON . stringify ( urlOrUrls ) : null ;
44+ } , [ urlOrUrls ] ) ;
45+
46+ useEffect ( ( ) => {
47+ const isArray = Array . isArray ( urlOrUrls ) ;
48+ if ( ! urlOrUrls || ( isArray && urlOrUrls . length === 0 ) ) {
49+ setData ( null ) ;
50+ setLoading ( false ) ;
51+ return ;
52+ }
53+
54+ const abortController = new AbortController ( ) ;
55+ const fetchData = async ( ) => {
56+ setLoading ( true ) ;
57+ setError ( null ) ;
58+ setErrors ( [ ] ) ;
59+
60+ try {
61+ let resultData ;
62+ if ( isArray ) {
63+ // Used Promise.allSettled to handle partial failures
64+ const settledResults = await Promise . allSettled (
65+ urlOrUrls . map ( async ( u ) => {
66+ if ( cache . has ( u ) ) return { url : u , data : cache . get ( u ) } ;
67+ const response = await axiosInstance . get ( u , { signal : abortController . signal } ) ;
68+ cache . set ( u , response . data ) ;
69+ return { url : u , data : response . data } ;
70+ } )
71+ ) ;
72+
73+ const successfulData = { } ;
74+ const failedRequests = [ ] ;
75+ settledResults . forEach ( result => {
76+ if ( result . status === 'fulfilled' ) {
77+ successfulData [ result . value . url ] = result . value . data ;
78+ } else {
79+ failedRequests . push ( result . reason ) ;
80+ }
81+ } ) ;
82+ resultData = successfulData ;
83+ if ( failedRequests . length > 0 ) {
84+ setErrors ( failedRequests ) ;
85+ // Set the primary error to the first one for convenience.
86+ setError ( failedRequests [ 0 ] ) ;
87+ }
88+ } else {
89+ // Handle single URL
90+ const singleUrl = urlOrUrls ;
91+ if ( cache . has ( singleUrl ) ) {
92+ resultData = cache . get ( singleUrl ) ;
93+ } else {
94+ const response = await axiosInstance . get ( singleUrl , { signal : abortController . signal } ) ;
95+ cache . set ( singleUrl , response . data ) ;
96+ resultData = response . data ;
97+ }
98+ }
99+
100+ if ( urlsKey === JSON . stringify ( requestRef . current ) ) {
101+ setData ( resultData ) ;
102+ }
103+ } catch ( err ) {
104+ //catch error from url(s)
105+ if ( err . name !== 'CanceledError' && urlsKey === JSON . stringify ( requestRef . current ) ) {
106+ setError ( err ) ;
107+ setErrors ( [ err ] ) ;
108+ }
109+ } finally {
110+ if ( urlsKey === JSON . stringify ( requestRef . current ) ) {
111+ setLoading ( false ) ;
112+ }
113+ }
114+ } ;
115+
116+ fetchData ( ) ;
117+ return ( ) => abortController . abort ( ) ;
118+ } , [ urlsKey , forceRefetch ] ) ; //intentional
119+
120+ const refetch = ( ) => {
121+ if ( Array . isArray ( urlOrUrls ) ) {
122+ urlOrUrls . forEach ( u => invalidateCache ( u ) ) ;
123+ } else if ( urlOrUrls ) {
124+ invalidateCache ( urlOrUrls ) ;
125+ }
126+ setForceRefetch ( Date . now ( ) ) ; // Trigger a re-run of the effect
127+ } ;
128+
129+ return { data, loading, error, errors, refetch } ;
130+ } ;
131+
132+ export default useFetchnCache ;
0 commit comments