utilcn logoutilcn
Storage

uploadFile

A React component and hook for file uploads with progress tracking

Frontend Implementation

This component is designed for frontend use and handles file uploads through presigned URLs. It pairs with the generate-presigned-upload-url function which should be implemented on your backend server to generate secure upload URLs.

Usage

Component

import { UploadFile } from '@/lib/upload-file';

export function MyPage() {
  return (
    <div>
      <h2>Upload your files</h2>
      <UploadFile />
    </div>
  );
}

Hook

import { useUploadFile } from '@/hooks/use-upload-file';

export function CustomUploadComponent() {
  const { uploadFile } = useUploadFile();

  const handleUpload = (file: File) => {
    uploadFile({
      file,
      onSuccess: (url) => console.log('Uploaded to:', url),
      onError: (err) => console.error('Upload failed:', err),
      onProgress: (percent) => console.log(`Progress: ${percent}%`),
    });
  };

  return (
    <input
      type="file"
      onChange={(e) => {
        const file = e.target.files?.[0];
        if (file) handleUpload(file);
      }}
    />
  );
}

Installation

pnpm dlx shadcn@latest add https://utilcn.dev/r/upload-file.json
bunx --bun shadcn@latest add https://utilcn.dev/r/upload-file.json
npx shadcn@latest add https://utilcn.dev/r/upload-file.json
yarn shadcn@latest add https://utilcn.dev/r/upload-file.json

Hook API

Parameters

The useUploadFile hook returns an object with an uploadFile function that accepts:

ParameterTypeDescription
fileFileThe file to upload
onProgress(percent: number) => voidOptional progress callback
onSuccess(fileUrl: string) => voidOptional success callback with file URL
onError(error: Error) => voidOptional error callback

API Integration

generatePresignedUploadUrl

Generate presigned URLs for secure file uploads to cloud storage

Implementation

UploadFile Component

'use client';

import { type ChangeEvent, useState } from 'react';
import { useUploadFile } from '@/hooks/use-upload-file';

export function UploadFile() {
  const [progress, setProgress] = useState(0);
  const [isUploading, setIsUploading] = useState(false);
  const { uploadFile } = useUploadFile();

  const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) {
      return;
    }

    setIsUploading(true);
    uploadFile({
      file,
      onProgress: (p: number) => setProgress(p),
      onSuccess: (url: string) => {
        console.log('Uploaded to:', url);
        setIsUploading(false);
        setProgress(0);
      },
      onError: (err: Error) => {
        console.error(`Upload failed: ${err.message}`);
        setIsUploading(false);
        setProgress(0);
      },
    });
  };

  return (
    <div className="flex flex-col gap-4">
      <input
        className="file:mr-4 file:rounded-md file:border-0 file:bg-secondary file:px-4 file:py-2 file:font-semibold file:text-secondary-foreground file:text-sm hover:file:bg-secondary/80"
        onChange={handleFileChange}
        type="file"
      />

      {isUploading && (
        <div className="mt-4 flex items-center gap-2">
          <div className="h-2 w-full rounded-full bg-secondary">
            <div
              className="h-2 rounded-full bg-primary transition-all duration-300"
              style={{ width: `${progress}%` }}
            />
          </div>
          <span className="text-muted-foreground text-sm">{progress}%</span>
        </div>
      )}
    </div>
  );
}

useUploadFile Hook

import { useCallback } from 'react';

type UploadArgs = {
  file: File;
  onProgress?: (percent: number) => void;
  onSuccess?: (fileUrl: string) => void;
  onError?: (error: Error) => void;
};

export function useUploadFile() {
  const uploadFile = useCallback(
    async ({ file, onProgress, onSuccess, onError }: UploadArgs) => {
      try {
        const presignRes = await fetch('http://localhost:8080/uploadFile', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            fileName: file.name,
            contentLength: file.size,
          }),
        });

        if (!presignRes.ok) {
          throw new Error('Failed to get presigned URL');
        }
        const presign = await presignRes.json();

        await new Promise<void>((resolve, reject) => {
          const xhr = new XMLHttpRequest();
          xhr.open('PUT', presign.uploadUrl);

          xhr.setRequestHeader('Content-Type', file.type);

          xhr.upload.onprogress = (evt) => {
            if (evt.lengthComputable && onProgress) {
              const PERCENTAGE_MULTIPLIER = 100;
              const percent = Math.round(
                (evt.loaded * PERCENTAGE_MULTIPLIER) / evt.total,
              );
              onProgress(percent);
            }
          };

          xhr.onload = () => {
            if (xhr.status >= 200 && xhr.status < 300) {
              resolve();
            } else {
              reject(new Error('Upload failed'));
            }
          };

          xhr.onerror = () => reject(new Error('Upload failed'));
          xhr.send(file);
        });

        const fileUrl = presign.fileUrl as string;
        onSuccess?.(fileUrl);
        return fileUrl;
      } catch (error) {
        const uploadError =
          error instanceof Error ? error : new Error('Upload failed');
        onError?.(uploadError);
        throw uploadError;
      }
    },
    [],
  );

  return { uploadFile };
}