useQuery and Caching

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:

src/api/books.ts: the fetch helpers and query keys (excerpt)TypeScript
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:

src/app/book/[id].tsx: reading a book through the cache (excerpt)JavaScript
  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:

Output of 162
'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.