Rules Hub

Coding Rules Library

← Back to all rules
fullstack ruleStack: react
architectureclean-codetanstack-queryapidependency-injection

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.

PR: Feat/FCK-2245 - Cache Bellsheep and profile loaders with TanStack Query #343Created: Dec 10, 2025

Bad example

Old codets
1// api.ts: Simple wrapper, no caching awareness
2export 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
9export 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

New codets
1// api.ts: Caching and signal merging encapsulated
2export 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
21export 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.