> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Data Fetching

> Fetch and mutate backend data from the SPA with TanStack Query, where hooks are the only layer that talks to the backend.

The SPA data layer is built on [TanStack Query](https://tanstack.com/query) (`@tanstack/react-query`). It owns server state — the data your app reads from and writes to the backend — with caching, background refetching, request cancellation, and cache invalidation handled for you.

The architectural rule is simple and strict: **hooks are the only layer that talks to the backend.** Data fetching and API calls live in a feature's `hooks/` (or `shared/hooks/`). `services/` hold pure domain logic and never touch the backend. `store/` holds client state, not server data. Keeping these layers separate means your fetch logic, your business rules, and your UI state never bleed into one another.

These hooks call the same backend you build with controllers and routing — they hit `/api/...` endpoints. See [routing](/basics/routing) and [controller](/basics/controller) for the server side of that contract.

## Where the QueryClient lives

TanStack Query needs a `QueryClient` provided at the root of the app. That provider is wired once at bootstrap in `src/bootstrap/app.tsx`, so every hook in every feature can read and write the shared cache. You don't set this up per feature — it's there from the moment the app starts.

When you run [`spa:feature:create`](/spa/features), `@tanstack/react-query` is installed automatically and the generator scaffolds two example hooks per feature — a read hook and a mutation hook — plus a query-key factory. You start from working code, not a blank file.

## How it works

| Concept                | What it does                                                                                                         |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
| **Queries**            | Read data with `useQuery`. Results are cached by query key and refetched in the background when stale.               |
| **Mutations**          | Write data with `useMutation`. On success you update or invalidate the cache so reads reflect the change.            |
| **Query keys**         | A serializable array that identifies a cached entry. A query-key factory keeps reads and invalidations in agreement. |
| **Cache invalidation** | After a write, `invalidateQueries` marks matching entries stale so they refetch with fresh server data.              |
| **Cancellation**       | The `signal` passed to a query is forwarded to `fetch`, cancelling in-flight requests on unmount or refetch.         |

## The read hook

A read hook wraps `useQuery`. The generated `useGetSettings` hook below shows the full shape: a typed result, a query-key factory, a fetch function that takes an `AbortSignal`, and the hook itself.

```typescript theme={null}
import { useQuery } from "@tanstack/react-query";

export type SettingsType = { id: string; name: string };

// Query key factory — single source of truth for this feature's keys so reads
// and invalidations always agree.
export const settingsKeys = {
  all: ["settings"] as const,
  details: () => [...settingsKeys.all, "detail"] as const,
  detail: (id: string) => [...settingsKeys.details(), id] as const,
};

const getSettings = async (id: string, signal: AbortSignal): Promise<SettingsType> => {
  const response = await fetch(`/api/settings/${id}`, { signal });
  if (!response.ok) throw new Error("Failed to fetch settings");
  return (await response.json()) as SettingsType;
};

export const useGetSettings = (id: string) => {
  return useQuery({
    queryKey: settingsKeys.detail(id),
    queryFn: ({ signal }) => getSettings(id, signal), // signal cancels on unmount/refetch
    enabled: Boolean(id),                              // wait until an id is available
  });
};
```

Three patterns are worth calling out:

* **The query-key factory.** `settingsKeys` is the single source of truth for this feature's keys. `all` is the broad key (`["settings"]`), `details()` narrows to detail-type entries, and `detail(id)` identifies one record. Because reads and invalidations both derive their keys from this object, they can never drift apart.
* **The `signal`.** `useQuery` hands a fresh `AbortSignal` to `queryFn`. Forwarding it to `fetch` means a request is cancelled automatically when the component unmounts or the query refetches — no stale responses, no wasted work.
* **`enabled`.** Setting `enabled: Boolean(id)` defers the query until an `id` is actually available. The hook stays idle instead of firing a request for `undefined`.

A component consumes the hook directly — no service call, no manual fetch:

```typescript theme={null}
const { data, isPending, isError } = useGetSettings(settingsId);

if (isPending) return <Spinner />;
if (isError) return <ErrorState />;
return <SettingsView settings={data} />;
```

## The mutation hook

A mutation hook wraps `useMutation`. It imports the query-key factory and types from the read hook, so the write side reuses the exact keys the read side cached under.

```typescript theme={null}
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { settingsKeys, type SettingsType } from "./useGetSettings";

type UpdateSettingsInputType = Partial<Omit<SettingsType, "id">> & { id: string };

const updateSettings = async ({ id, ...input }: UpdateSettingsInputType): Promise<SettingsType> => {
  const response = await fetch(`/api/settings/${id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(input),
  });
  if (!response.ok) throw new Error("Failed to update settings");
  return (await response.json()) as SettingsType;
};

export const useUpdateSettings = () => {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: updateSettings,
    onSuccess: (data) => {
      // Seed the cache with the server response, then invalidate to refetch siblings.
      queryClient.setQueryData(settingsKeys.detail(data.id), data);
      return queryClient.invalidateQueries({ queryKey: settingsKeys.all });
    },
  });
};
```

The `onSuccess` handler is the heart of keeping the cache honest after a write:

* **`setQueryData`** writes the server's response straight into the detail cache so the updated record is available immediately, with no extra round trip.
* **`invalidateQueries`** marks every entry under `settingsKeys.all` stale, so lists and related views refetch fresh data.
* **Returning the promise** from `invalidateQueries` keeps the mutation in its pending state until the refetch settles. Your UI can show a spinner until the screen is fully consistent, not just until the write returned.

Calling a mutation from a component mirrors the read side:

```typescript theme={null}
const { mutate, isPending } = useUpdateSettings();

const onSubmit = (values: { name: string }) => {
  mutate({ id: settingsId, ...values });
};
```

## Best practices

* **Centralize keys in a factory.** Define one query-key factory per feature (`all` / `details` / `detail`) and derive every key from it. Reads and invalidations stay in sync because they share the same source.
* **Keep fetch logic in hooks only.** All backend calls belong in `hooks/` (or `shared/hooks/`). Never call `fetch` from components, services, or stores.
* **Keep services pure.** `services/` hold domain logic and never touch the network. If a service needs data, the hook fetches it and passes it in.
* **Invalidate by key after writes.** In a mutation's `onSuccess`, invalidate the affected keys so dependent reads refetch. Return the invalidation promise to keep the mutation pending until the cache is consistent.
* **Pass the `signal`.** Forward the `AbortSignal` from `queryFn` to `fetch` so requests cancel on unmount and refetch.
* **Use `enabled` to gate on params.** Defer a query until its inputs (an id, a filter) are ready instead of firing requests for missing values.
* **Let the generator scaffold it.** `spa:feature:create` writes a read hook, a mutation hook, and a key factory following these patterns — start there and adapt.

## See also

* [SPA overview](/spa/overview) — the big picture of the single page app.
* [Project structure](/spa/structure) — where `hooks/`, `services/`, and `store/` live.
* [Features](/spa/features) — generating features and their hooks with `spa:feature:create`.
* [SPA routing](/spa/routing) — wiring features into screens.
* [Routing](/basics/routing) and [Controller](/basics/controller) — the backend endpoints these hooks call.
* [Fetcher](/utilities/fetcher) — the HTTP utility for talking to your API.
