TanStack Query
Invalidate queries after a confirmed upload. Do not wrap useUpload in useMutation.
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
"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);
await queryClient.invalidateQueries({ queryKey: ["profile"] });
},
});
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
With several files per call, invalidate once in onSettled. It runs once per
call, after every onSuccess:
const upload = useUpload({
route: "attachments",
maxFiles: 10,
onSettled: async ({ uploaded }) => {
if (uploaded.length === 0) return;
await queryClient.invalidateQueries({ queryKey: ["attachments"] });
},
});Forms
For form fields that store object keys, see Forms.
What not to do
// Avoid — loses per-file progress and double-tracks pending state
const mutation = useMutation({
mutationFn: (file: File) => upload.handleFiles(file),
}); 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.