Rules Hub
Coding Rules Library
Integrate caching and dependency injection into fetch utilities
Encapsulate caching logic (like ensureQueryData) and dependency injection within fetch helpers to reduce boilerplate and ensure consistent signal handling.
Bad example
| 1 | // api.ts: Simple wrapper, no caching awareness |
| 2 | export async function fetchApi<T>(url: string, signal?: AbortSignal) { |
| 3 | const res = await fetch(url, { signal }); |
| 4 | if (!res.ok) throw new Error('Failed'); |
| 5 | return res.json() as T; |
| 6 | } |
| 7 |
|
| 8 | // loader.ts: Consumer has to write boilerplate |
| 9 | export async function loader({ request, context }: LoaderArgs) { |
| 10 | const { queryClient } = context; |
| 11 | // Boilerplate: manually defining queryKey and wrapping in ensureQueryData |
| 12 | return queryClient.ensureQueryData({ |
| 13 | queryKey: ['/api/data'], |
| 14 | queryFn: ({ signal }) => fetchApi('/api/data', signal), |
| 15 | }); |
| 16 | } |
Explanation (EN)
The fetch utility is too simple, forcing every consumer to manually wrap calls with `ensureQueryData`, manage query keys, and manually pass abort signals. This creates repetitive boilerplate and increases the risk of forgetting to forward signals.
Objašnjenje (HR)
Uslužna funkcija za dohvaćanje je prejednostavna, prisiljavajući svakog potrošača da ručno omata pozive s `ensureQueryData`, upravlja ključevima upita i ručno prosljeđuje signale za prekid. To stvara ponavljajući kod i povećava rizik od zaboravljanja prosljeđivanja signala.
Good example
| 1 | // api.ts: Caching and signal merging encapsulated |
| 2 | export async function fetchApi<T>( |
| 3 | url: string, |
| 4 | options: { queryClient?: QueryClient; signal?: AbortSignal; queryKey?: QueryKey } |
| 5 | ) { |
| 6 | const { queryClient, signal, queryKey } = options; |
| 7 | const doFetch = (s?: AbortSignal) => fetch(url, { signal: s }).then(r => r.json()); |
| 8 |
|
| 9 | if (queryClient) { |
| 10 | return queryClient.ensureQueryData({ |
| 11 | queryKey: queryKey ?? [url], |
| 12 | queryFn: ({ signal: querySignal }) => |
| 13 | // Merge external signal (navigation) with internal signal (timeout/unmount) |
| 14 | doFetch(mergeAbortSignals(signal, querySignal)), |
| 15 | }); |
| 16 | } |
| 17 | return doFetch(signal); |
| 18 | } |
| 19 |
|
| 20 | // loader.ts: Clean usage |
| 21 | export async function loader({ request, context }: LoaderArgs) { |
| 22 | return fetchApi('/api/data', { |
| 23 | queryClient: context.queryClient, |
| 24 | signal: request.signal, |
| 25 | }); |
| 26 | } |
Explanation (EN)
The fetch utility now accepts a `QueryClient` instance and handles the caching logic internally. It automatically uses the URL as the default query key and safely merges the navigation abort signal with the query client's signal, reducing boilerplate at usage sites.
Objašnjenje (HR)
Uslužna funkcija sada prihvaća `QueryClient` instancu i interno upravlja logikom predmemoriranja. Automatski koristi URL kao zadani ključ upita i sigurno spaja signal za prekid navigacije sa signalom klijenta upita, smanjujući ponavljajući kod na mjestima korištenja.
Notes (EN)
Pass `QueryClient` as an argument (dependency injection) instead of importing a global instance to ensure compatibility with Server-Side Rendering (SSR) and isolation in tests.
Bilješke (HR)
Prosljeđujte `QueryClient` kao argument (injekcija ovisnosti) umjesto uvoza globalne instance kako biste osigurali kompatibilnost sa serverskim renderiranjem (SSR) i izolaciju u testovima.