Загрузка файлов в TanStack Query строится вокруг мутаций, поскольку операция относится к изменению состояния сервера и не является чистым чтением данных. Основная цель — связать процесс отправки файлов с управляемым жизненным циклом запроса, сохранив контроль над состоянием загрузки, ошибками, повторными попытками и синхронизацией кеша.
Загрузка файлов почти всегда опирается на FormData, так
как именно этот формат поддерживает multipart-запросы, необходимые для
передачи бинарных данных.
import { useMutation } from '@tanstack/react-query'
async function uploadFile(file) {
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
if (!response.ok) {
throw new Error('Ошибка загрузки файла')
}
return response.json()
}
export function useFileUpload() {
return useMutation({
mutationFn: uploadFile,
})
}
Мутация в таком виде уже предоставляет базовую инфраструктуру: состояние загрузки, обработку ошибок и возможность интеграции с кешем запросов.
TanStack Query автоматически управляет состояниями:
isPending — загрузка выполняетсяisSuccess — загрузка завершена успешноisError — произошла ошибкаdata — результат сервераconst upload = useFileUpload()
upload.mutate(file, {
onSuccess: (data) => {
console.log('Файл загружен', data)
},
onError: (error) => {
console.log('Ошибка', error)
}
})
Состояние мутации используется для построения интерфейсов прогресса, блокировки кнопок и отображения статусов.
Fetch API не предоставляет удобного прогресса загрузки, поэтому часто используется axios.
import axios from 'axios'
async function uploadFile(file, onProgress) {
const formData = new FormData()
formData.append('file', file)
const response = await axios.post('/api/upload', formData, {
onUploadProgress: (event) => {
const percent = Math.round((event.loaded * 100) / event.total)
onProgress(percent)
},
})
return response.data
}
Интеграция с TanStack Query:
export function useFileUpload() {
return useMutation({
mutationFn: ({ file, onProgress }) => uploadFile(file, onProgress),
})
}
Прогресс часто хранится вне кеша TanStack Query, так как он является временным и частым состоянием.
const upload = useFileUpload()
const [progress, setProgress] = useState(0)
const handleUpload = (file) => {
upload.mutate({
file,
onProgress: setProgress,
})
}
UI может реагировать на процент загрузки, комбинируя его с состоянием мутации:
progress < 100 — активная загрузкаisSuccess — завершеноisError — ошибкаЧасто требуется отображать файл в списке сразу после выбора, до завершения загрузки.
const queryClient = useQueryClient()
const upload = useMutation({
mutationFn: uploadFile,
onMutate: async (file) => {
await queryClient.cancelQueries({ queryKey: ['files'] })
const previous = queryClient.getQueryData(['files'])
const optimisticFile = {
id: Date.now(),
name: file.name,
status: 'uploading',
}
queryClient.setQueryData(['files'], (old = []) => [
...old,
optimisticFile,
])
return { previous, optimisticFile }
},
onError: (_err, _file, context) => {
queryClient.setQueryData(['files'], context.previous)
},
onSuccess: (data, _file, context) => {
queryClient.setQueryData(['files'], (old = []) =>
old.map((f) =>
f.id === context.optimisticFile.id ? data : f
)
)
},
})
Такой подход создаёт ощущение мгновенной реакции интерфейса.
Если список файлов полностью управляется сервером, проще выполнить инвалидацию:
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['files'] })
}
Инвалидация гарантирует консистентность данных, особенно если сервер добавляет метаданные (размер, URL, тип).
Ошибки загрузки могут возникать на разных этапах: сеть, сервер, валидация файла.
const upload = useMutation({
mutationFn: uploadFile,
retry: 2,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 8000),
})
Повторные попытки особенно полезны при нестабильной сети.
При необходимости можно отключить повтор:
retry: false
Для больших файлов важно уметь прерывать загрузку.
async function uploadFile(file, signal) {
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
signal,
})
if (!response.ok) {
throw new Error('Ошибка загрузки')
}
return response.json()
}
Использование AbortController:
const controller = new AbortController()
upload.mutate({
file,
signal: controller.signal,
})
// отмена
controller.abort()
При загрузке нескольких файлов каждая мутация должна быть независимой.
files.forEach((file) => {
upload.mutate(file)
})
Для отслеживания общего состояния часто используется внешний массив:
Для ограничения нагрузки используется очередь:
const queue = []
let active = 0
const MAX = 3
function next() {
if (active >= MAX || queue.length === 0) return
const file = queue.shift()
active++
upload.mutate(file, {
onSettled: () => {
active--
next()
},
})
next()
}
Такой подход полезен при больших пакетных загрузках.
Разбиение файла на части повышает устойчивость и позволяет возобновлять загрузку.
function createChunks(file, size = 2 * 1024 * 1024) {
const chunks = []
let start = 0
while (start < file.size) {
chunks.push(file.slice(start, start + size))
start += size
}
return chunks
}
Каждый чанк отправляется отдельной мутацией:
async function uploadChunk(chunk, index) {
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('index', index)
return fetch('/api/upload-chunk', {
method: 'POST',
body: formData,
})
}
Сборка файла на сервере выполняется отдельно после завершения всех частей.
TanStack Query позволяет хранить статус загрузки в кеше:
queryClient.setQueryData(['upload-status', fileId], {
progress,
status: 'uploading',
})
Это полезно, если прогресс должен отображаться в разных компонентах интерфейса.
При разрыве соединения важно сохранять состояние:
const state = queryClient.getQueryData(['upload-session', fileId])
При восстановлении можно продолжить с последнего успешного чанка.
После загрузки файл обычно добавляется в список сущностей:
onSuccess: (data) => {
queryClient.setQueryData(['files'], (old = []) => [
data,
...old,
])
}
Это уменьшает необходимость повторной загрузки всего списка.
Типовая архитектура включает:
Разделение позволяет масштабировать систему до сложных сценариев: массовые загрузки, фоновые задачи, восстановление сессий и распределённая загрузка больших файлов.