BookNest's API calls live in one module, with a keys object (keys.book(3) is ['book', 3]) so screens and mutations always agree on query keys:
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API}${path}`, init);
if (!res.ok) throw new Error(`${init?.method ?? 'GET'} ${path} failed: ${res.status}`);
return res.json() as Promise<T>;
}
export const getBook = (id: number) => request<ApiBook>(`/books/${id}`);fetch only rejects on network failures, so the helper turns HTTP errors into exceptions too; otherwise a 500 would be cached as data. API comes from EXPO_PUBLIC_API_URL in .env (Env and app.config). The book screen asks for one book:
const id = Number(useLocalSearchParams<'/book/[id]'>().id);
const { data: book, isPending, fetchStatus, dataUpdatedAt } = useQuery({
queryKey: keys.book(id),
queryFn: () => getBook(id),
});Opening book 3, going back and opening it again, with the logs from the app and from the server:
'book 3: data, fetch fetching,', 'updated 16:13:17' 'path:', '/book/3' 'book 3: data, fetch idle,', 'updated 16:15:46' 'path:', '/' 'book 3: data, fetch idle,', 'updated 16:15:46' 'path:', '/book/3' 16:15:46 GET /books/3 200
The first visit rendered at once with a copy from 16:13, restored from the persisted cache, and refetched in the background because that copy was stale; this pattern is called stale-while-revalidate. The second visit, within the minute of staleTime, used the cache and sent no request. status (pending, error, success) says whether you have data; fetchStatus (fetching, paused, idle) says whether a request is running. Keep them apart: a screen with data can be fetching, and a screen without data can be paused offline.