šŸ“ Sign Up | šŸ” Log In

← Root | ↑ Up

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ šŸ“„ shadcn/directory/brennenrocks/utilcn/storage/upload-file │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

╔══════════════════════════════════════════════════════════════════════════════════════════════╗
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘

title: uploadFile description: A React component and hook for file uploads with progress tracking

<Callout title="Frontend Implementation" type="info"> 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. </Callout>

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

<InstallTabs component="upload-file" />

Hook API

Parameters

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

| Parameter | Type | Description | |--------------|-----------------------------------|---------------------------------------| | file | File | The file to upload | | onProgress | (percent: number) => void | Optional progress callback | | onSuccess | (fileUrl: string) => void | Optional success callback with file URL | | onError | (error: Error) => void | Optional error callback |

API Integration

<Card title="generatePresignedUploadUrl" href="/docs/storage/generate-presigned-upload-url"> Generate presigned URLs for secure file uploads to cloud storage </Card>

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 };
}

ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•‘
ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•

← Root | ↑ Up