Skip to content

React Integration

Torrin integrates seamlessly with React applications. This guide shows you how to build a complete upload component with progress tracking, pause/resume, and error handling.

Installation

bash
npm install @torrin-kit/client
bash
yarn add @torrin-kit/client
bash
pnpm add @torrin-kit/client
bash
bun add @torrin-kit/client

Basic Example

A simple file uploader with progress tracking:

tsx
import { useState } from 'react';
import { createTorrinClient } from '@torrin-kit/client';

const torrin = createTorrinClient({
  endpoint: 'http://localhost:3000/api/uploads',
});

export default function FileUploader() {
  const [progress, setProgress] = useState(0);
  const [status, setStatus] = useState('idle');

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

    const upload = torrin.createUpload({ file });

    upload.on('progress', (p) => {
      setProgress(p.percentage);
    });

    upload.on('status', (s) => {
      setStatus(s);
    });

    try {
      const result = await upload.start();
      console.log('Upload complete:', result.location);
    } catch (error) {
      console.error('Upload failed:', error);
    }
  };

  return (
    <div className="space-y-4">
      <input 
        type="file" 
        onChange={handleFileChange}
        className="block w-full text-sm"
      />
      
      {status !== 'idle' && (
        <div className="space-y-2">
          <div className="w-full bg-gray-200 rounded-full h-2.5">
            <div 
              className="bg-blue-600 h-2.5 rounded-full transition-all" 
              style={{ width: `${progress}%` }}
            />
          </div>
          <div className="flex justify-between text-sm">
            <span>{status}</span>
            <span>{progress.toFixed(1)}%</span>
          </div>
        </div>
      )}
    </div>
  );
}

Advanced Example

A complete uploader with pause, resume, and cancel functionality:

tsx
import { useState, useRef } from 'react';
import { createTorrinClient, type TorrinUpload } from '@torrin-kit/client';

const torrin = createTorrinClient({
  endpoint: 'http://localhost:3000/api/uploads',
});

export default function AdvancedUploader() {
  const [progress, setProgress] = useState(0);
  const [status, setStatus] = useState<string>('idle');
  const [error, setError] = useState<string | null>(null);
  const uploadRef = useRef<TorrinUpload | null>(null);

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

    setError(null);
    setProgress(0);

    const upload = torrin.createUpload({ 
      file,
      metadata: { 
        userId: 'user123',
        timestamp: new Date().toISOString() 
      }
    });

    uploadRef.current = upload;

    upload.on('progress', (p) => {
      setProgress(p.percentage);
      console.log(`${p.chunksCompleted}/${p.totalChunks} chunks uploaded`);
    });

    upload.on('status', (s) => {
      setStatus(s);
    });

    upload.on('error', (err) => {
      setError(err.message);
    });

    try {
      const result = await upload.start();
      console.log('Upload complete:', result);
      uploadRef.current = null;
    } catch (err: any) {
      setError(err.message);
    }
  };

  const handlePause = () => {
    uploadRef.current?.pause();
  };

  const handleResume = () => {
    uploadRef.current?.resume();
  };

  const handleCancel = async () => {
    await uploadRef.current?.cancel();
    uploadRef.current = null;
    setProgress(0);
    setStatus('idle');
  };

  return (
    <div className="max-w-md p-6 bg-white rounded-lg shadow">
      <h2 className="text-xl font-bold mb-4">Upload File</h2>
      
      <input 
        type="file" 
        onChange={handleFileChange}
        disabled={status === 'uploading'}
        className="block w-full mb-4"
      />

      {error && (
        <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded">
          <p className="text-sm text-red-600">{error}</p>
        </div>
      )}

      {status !== 'idle' && (
        <div className="space-y-4">
          <div>
            <div className="flex justify-between text-sm mb-1">
              <span className="font-medium">Status: {status}</span>
              <span>{progress.toFixed(1)}%</span>
            </div>
            <div className="w-full bg-gray-200 rounded-full h-3">
              <div 
                className="bg-blue-600 h-3 rounded-full transition-all duration-300" 
                style={{ width: `${progress}%` }}
              />
            </div>
          </div>

          <div className="flex gap-2">
            {status === 'uploading' && (
              <button
                onClick={handlePause}
                className="px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600"
              >
                Pause
              </button>
            )}
            
            {status === 'paused' && (
              <button
                onClick={handleResume}
                className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
              >
                Resume
              </button>
            )}
            
            {(status === 'uploading' || status === 'paused') && (
              <button
                onClick={handleCancel}
                className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
              >
                Cancel
              </button>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

Custom Hook

Create a reusable hook for upload management:

typescript
import { useState, useCallback, useRef } from 'react';
import { createTorrinClient, type TorrinUpload } from '@torrin-kit/client';

const torrin = createTorrinClient({
  endpoint: 'http://localhost:3000/api/uploads',
});

export function useTorrinUpload() {
  const [progress, setProgress] = useState(0);
  const [status, setStatus] = useState<string>('idle');
  const [error, setError] = useState<string | null>(null);
  const uploadRef = useRef<TorrinUpload | null>(null);

  const startUpload = useCallback(async (file: File, metadata?: Record<string, any>) => {
    setError(null);
    setProgress(0);

    const upload = torrin.createUpload({ file, metadata });
    uploadRef.current = upload;

    upload.on('progress', (p) => setProgress(p.percentage));
    upload.on('status', (s) => setStatus(s));
    upload.on('error', (err) => setError(err.message));

    try {
      return await upload.start();
    } catch (err: any) {
      setError(err.message);
      throw err;
    }
  }, []);

  const pause = useCallback(() => {
    uploadRef.current?.pause();
  }, []);

  const resume = useCallback(() => {
    uploadRef.current?.resume();
  }, []);

  const cancel = useCallback(async () => {
    await uploadRef.current?.cancel();
    uploadRef.current = null;
    setProgress(0);
    setStatus('idle');
  }, []);

  return {
    progress,
    status,
    error,
    startUpload,
    pause,
    resume,
    cancel,
  };
}

Usage:

tsx
function MyUploader() {
  const { progress, status, error, startUpload, pause, resume, cancel } = useTorrinUpload();

  const handleFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      try {
        const result = await startUpload(file, { userId: '123' });
        console.log('Done:', result);
      } catch (err) {
        console.error('Failed:', err);
      }
    }
  };

  return (
    <div>
      <input type="file" onChange={handleFile} />
      <p>Progress: {progress}%</p>
      <p>Status: {status}</p>
      {error && <p>Error: {error}</p>}
      <button onClick={pause}>Pause</button>
      <button onClick={resume}>Resume</button>
      <button onClick={cancel}>Cancel</button>
    </div>
  );
}

With Resume Support

Enable automatic resume after page refresh:

typescript
import { createTorrinClient, createLocalStorageResumeStore } from '@torrin-kit/client';

const torrin = createTorrinClient({
  endpoint: 'http://localhost:3000/api/uploads',
  resumeStore: createLocalStorageResumeStore(), // Enable auto-resume
});

Now if the user refreshes the page and selects the same file, the upload will automatically resume from where it left off.

Next Steps