FluxMedia
Copy page
Open in ChatGPTOpen in Claude

React Integration

Hooks and components for React applications.

Installation

pnpm add @fluxmedia/core @fluxmedia/react

useMediaUpload Hook

The primary hook for handling uploads in React:

import { useMediaUpload } from '@fluxmedia/react';

function UploadForm() {
  const { upload, uploading, progress, result, error, reset } = useMediaUpload({
    mode: 'signed',
    signUrlEndpoint: '/api/upload/sign',
    onUploadComplete: (result) => console.log('Done:', result.url),
    onUploadError: (error) => console.error('Failed:', error),
  });

  return (
    <div>
      <input
        type="file"
        accept="image/*"
        disabled={uploading}
        onChange={(e) => {
          const file = e.target.files?.[0];
          if (file) upload(file, { folder: 'uploads' });
        }}
      />
      
      {uploading && <progress value={progress} max={100} />}
      {result && <img src={result.url} alt="Uploaded" />}
      {error && <p>Error: {error.message}</p>}
      
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Hook Options

interface UseMediaUploadConfig {
  mode: 'direct' | 'signed' | 'proxy';
  signUrlEndpoint?: string;  // For 'signed' mode
  proxyEndpoint?: string;    // For 'proxy' mode
  defaultOptions?: {
    folder?: string;
    tags?: string[];
  };
  onUploadStart?: () => void;
  onUploadComplete?: (result: UploadResult) => void;
  onUploadError?: (error: Error) => void;
}

Hook Return Values

interface UseMediaUploadReturn {
  upload: (file: File, options?: UploadOptions) => Promise<UploadResult>;
  uploading: boolean;
  progress: number; // 0-100
  result: UploadResult | null;
  error: Error | null;
  reset: () => void;
  
  // Preview functionality
  preview: string | null;          // Object URL for preview
  setPreview: (file: File | null) => void;  // Set preview from file
  
  // File type detection (magic bytes)
  fileType: { mime: string; ext: string } | null;
  detectFileType: (file: File) => Promise<{ mime: string; ext: string } | null>;
}

Preview Before Upload

Show a preview of selected files before uploading:

function UploadWithPreview() {
  const { upload, preview, setPreview, uploading } = useMediaUpload({
    mode: 'proxy',
    proxyEndpoint: '/api/upload',
  });

  const handleSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      setPreview(file);  // Creates object URL, handles cleanup
      upload(file);
    }
  };

  return (
    <div>
      {preview && <img src={preview} alt="Preview" />}
      <input type="file" accept="image/*" onChange={handleSelect} />
    </div>
  );
}

File Type Detection

Detect actual file type using magic bytes (more reliable than file extension):

function ValidatedUpload() {
  const { upload, detectFileType, fileType } = useMediaUpload({
    mode: 'proxy',
    proxyEndpoint: '/api/upload',
  });

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

    const type = await detectFileType(file);
    
    if (!type?.mime.startsWith('image/')) {
      alert('Please select an image file');
      return;
    }

    await upload(file);
  };

  return (
    <div>
      <input type="file" onChange={handleSelect} />
      {fileType && <p>Detected: {fileType.mime}</p>}
    </div>
  );
}

Provider Selection

Pass provider selection to upload:

function MultiProviderUpload() {
  const [provider, setProvider] = useState<'cloudinary' | 's3' | 'r2'>('cloudinary');
  const { upload } = useMediaUpload({
    mode: 'proxy',
    proxyEndpoint: '/api/upload',
  });

  const handleUpload = (file: File) => {
    upload(file, {
      folder: 'uploads',
      provider,  // Passed to server endpoint
      metadata: { description: 'User upload' },  // Custom metadata
    });
  };

  return (
    <div>
      <select value={provider} onChange={(e) => setProvider(e.target.value as any)}>
        <option value="cloudinary">Cloudinary</option>
        <option value="s3">AWS S3</option>
        <option value="r2">Cloudflare R2</option>
      </select>
      <input type="file" onChange={(e) => handleUpload(e.target.files![0])} />
    </div>
  );
}

Upload Modes

Browser gets a signed URL from your server, then uploads directly to the provider.

const { upload } = useMediaUpload({
  mode: 'signed',
  signUrlEndpoint: '/api/upload/sign',
});

Server endpoint (Next.js):

// app/api/upload/sign/route.ts
import crypto from 'crypto';

export async function POST(request: Request) {
  const { filename, folder } = await request.json();
  
  const timestamp = Math.round(Date.now() / 1000);
  const signature = crypto
    .createHash('sha1')
    .update(`folder=${folder}&timestamp=${timestamp}${apiSecret}`)
    .digest('hex');

  return Response.json({
    uploadUrl: `https://api.cloudinary.com/v1_1/${cloudName}/auto/upload`,
    fields: { api_key, timestamp, signature, folder },
  });
}

Proxy Upload

Upload goes through your server (more control, slightly slower).

const { upload } = useMediaUpload({
  mode: 'proxy',
  proxyEndpoint: '/api/upload',
});

Server endpoint:

// app/api/upload/route.ts
import { MediaUploader } from '@fluxmedia/core';
import { CloudinaryProvider } from '@fluxmedia/cloudinary';

export async function POST(request: Request) {
  const formData = await request.formData();
  const file = formData.get('file') as File;
  
  const uploader = new MediaUploader(new CloudinaryProvider({ ... }));
  const buffer = Buffer.from(await file.arrayBuffer());
  const result = await uploader.upload(buffer);
  
  return Response.json(result);
}

MediaUpload Component

A render-prop component for more complex UIs:

import { MediaUpload } from '@fluxmedia/react';

function FileUploader() {
  return (
    <MediaUpload
      config={{ mode: 'signed', signUrlEndpoint: '/api/upload/sign' }}
      accept="image/*"
      maxSize={5 * 1024 * 1024}
      onComplete={(results) => console.log('Uploaded:', results)}
      onError={(error) => console.error(error)}
    >
      {({ uploading, progress, result, error, openFileDialog }) => (
        <div>
          <button onClick={openFileDialog} disabled={uploading}>
            {uploading ? `Uploading ${progress}%` : 'Select File'}
          </button>
          {result && <img src={result.url} alt="Uploaded" />}
        </div>
      )}
    </MediaUpload>
  );
}

Component Props

Prop Type Description
config UseMediaUploadConfig Upload configuration
accept string Accepted file types
multiple boolean Allow multiple files
maxSize number Max file size in bytes
onSelect (files: File[]) => void Called when files selected
onComplete (results: UploadResult[]) => void Called after upload
onError (error: Error) => void Called on error