File Storage
useConvexUpload
High-level composable for file uploads with progress tracking.
High-level composable for file uploads with progress tracking. Auto-imported when storage is enabled.
Usage
app/pages/upload.vue
<script setup lang="ts">
import { api } from '#convex/api'
const { generateUploadUrl } = useConvexStorage(api)
const saveFile = useConvexMutation(api._hub.storage.saveFile)
const { upload, isUploading, progress, error } = useConvexUpload({
generateUploadUrl,
onSuccess: async (storageId, file) => {
await saveFile.mutate({
storageId,
name: file.name,
type: file.type,
userId: 'user_123',
})
},
onError: err => console.error('Upload failed:', err),
})
async function handleFileChange(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0]
if (file) {
const storageId = await upload(file)
console.log('Uploaded:', storageId)
}
}
</script>
<template>
<div>
<input type="file" :disabled="isUploading" @change="handleFileChange">
<p v-if="isUploading">
Uploading... {{ progress }}%
</p>
<p v-if="error">
{{ error.message }}
</p>
</div>
</template>
Options
| Option | Type | Description |
|---|---|---|
generateUploadUrl | { mutate: () => Promise<string> } | Required. From useConvexStorage |
onSuccess | (storageId: string, file: File) => void | Called after successful upload |
onError | (error: Error) => void | Called on upload error |
Return Values
| Property | Type | Description |
|---|---|---|
upload | (file: File) => Promise<string | null> | Upload a file, returns storageId |
isUploading | Ref<boolean> | True while uploading |
progress | Ref<number> | Upload progress 0-100 |
error | Ref<Error | null> | Error if upload failed |
Progress Bar Example
app/components/FileUpload.vue
<script setup lang="ts">
import { api } from '#convex/api'
const { generateUploadUrl } = useConvexStorage(api)
const { upload, isUploading, progress } = useConvexUpload({
generateUploadUrl,
onSuccess: (storageId) => {
console.log('File uploaded:', storageId)
},
})
</script>
<template>
<div>
<input type="file" :disabled="isUploading" @change="e => upload(e.target.files[0])">
<div v-if="isUploading" class="w-full bg-gray-200 rounded">
<div
class="bg-blue-500 h-2 rounded transition-all"
:style="{ width: `${progress}%` }"
/>
</div>
</div>
</template>
Multiple Files
Upload multiple files sequentially:
app/components/MultiUpload.vue
<script setup lang="ts">
import { api } from '#convex/api'
const { generateUploadUrl } = useConvexStorage(api)
const { upload, isUploading, progress } = useConvexUpload({ generateUploadUrl })
const uploadedFiles = ref<string[]>([])
async function handleFiles(event: Event) {
const files = (event.target as HTMLInputElement).files
if (!files)
return
for (const file of files) {
const storageId = await upload(file)
if (storageId)
uploadedFiles.value.push(storageId)
}
}
</script>
Image Preview
Preview images before upload:
app/components/ImageUpload.vue
<script setup lang="ts">
import { api } from '#convex/api'
const { generateUploadUrl } = useConvexStorage(api)
const { upload, isUploading } = useConvexUpload({ generateUploadUrl })
const preview = ref<string | null>(null)
function handleFileSelect(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0]
if (file && file.type.startsWith('image/')) {
preview.value = URL.createObjectURL(file)
}
}
async function handleUpload(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0]
if (file) {
await upload(file)
}
}
</script>
<template>
<div>
<img v-if="preview" :src="preview" class="max-w-xs">
<input type="file" accept="image/*" @change="handleFileSelect">
<button :disabled="isUploading" @click="handleUpload">
Upload
</button>
</div>
</template>