2.0 beta — new releases can include breaking changes. Stable 1.x docs
dimah-s3v2.0.0-beta.0

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

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); 
      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.

On this page