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

OptionTypeDescription
generateUploadUrl{ mutate: () => Promise<string> }Required. From useConvexStorage
onSuccess(storageId: string, file: File) => voidCalled after successful upload
onError(error: Error) => voidCalled on upload error

Return Values

PropertyTypeDescription
upload(file: File) => Promise<string | null>Upload a file, returns storageId
isUploadingRef<boolean>True while uploading
progressRef<number>Upload progress 0-100
errorRef<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>