# TanStack Query (https://v2.s3.dimah.dev/docs/react/tanstack-query)

`useUpload` already owns in-flight state: `isPending`, per-file progress,
errors, and retries. Wrapping `handleFiles` in `useMutation` duplicates that
state and loses per-file progress.

Use TanStack Query for **lists and records**. Let the upload hook's callbacks
save the key and invalidate those queries once the server confirms.

## Invalidate after confirm [#invalidate-after-confirm]

```tsx title="components/avatar-uploader.tsx"
"use client";

import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useUpload } from "@dimah-s3/react";
import { Upload, UploadStatus, UploadTrigger } from "@dimah-s3/ui";

async function fetchProfile() {
  const response = await fetch("/api/profile");
  return response.json() as Promise<{ avatarUrl: string | null }>;
}

async function saveAvatarKey(key: string) {
  await fetch("/api/profile", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ avatarKey: key }),
  });
}

export function AvatarUploader() {
  const queryClient = useQueryClient();
  const profile = useQuery({
    queryKey: ["profile"],
    queryFn: fetchProfile,
  });

  const upload = useUpload({
    route: "avatar",
    onSuccess: async (result) => {
      await saveAvatarKey(result.key); // [!code ++]
      await queryClient.invalidateQueries({ queryKey: ["profile"] }); // [!code ++]
    },
  });

  return (
    <div>
      {profile.data?.avatarUrl ? (
        <img src={profile.data.avatarUrl} alt="Current profile avatar" />
      ) : null}
      <Upload upload={upload}>
        <UploadTrigger />
        <UploadStatus />
      </Upload>
    </div>
  );
}
```

`onSuccess` runs once per file, after the server's `HeadObject` confirm. That
is the right time to save the key and refresh cached data.

## Many files [#many-files]

With several files per call, invalidate once in `onSettled`. It runs once per
call, after every `onSuccess`:

```ts
const upload = useUpload({
  route: "attachments",
  maxFiles: 10,
  onSettled: async ({ uploaded }) => {
    if (uploaded.length === 0) return;
    await queryClient.invalidateQueries({ queryKey: ["attachments"] });
  },
});
```

## Forms [#forms]

For form fields that store object keys, see [Forms](https://v2.s3.dimah.dev/docs/react/forms).

## What not to do [#what-not-to-do]

```tsx
// Avoid — loses per-file progress and double-tracks pending state
const mutation = useMutation({
  // [!code --]
  mutationFn: (file: File) => upload.handleFiles(file), // [!code --]
}); // [!code --]
```

`handleFiles` does not throw when a file fails or is rejected. It resolves
with `{ uploaded, failed, blocked, aborted }`, so a mutation would report
success. Keep Query for the data you read back, and `useUpload` for the
transfer.
