Installation
npm install @apexxcloud/react
yarn add @apexxcloud/react
useApexxCloud Hook
A custom hook for granular control over the upload process.Basic Usage
import { useApexxCloud } from "@apexxcloud/react";
import { useEffect, useRef } from "react";
import { useState } from "react";
export default function CustomUploader() {
const { upload, uploadMultipart } = useApexxCloud();
const [progress, setProgress] = useState(0);
const abortController = useRef(new AbortController());
const getSignedUrl = async (type: string, params: any) => {
const response = await fetch(
`http://localhost:5000/api/files/get-signed-url/${type}`,
{
method: "POST",
body: JSON.stringify(params),
headers: {
"Content-Type": "application/json",
},
}
);
const data = await response.json();
return data.url;
};
const handleUpload = async (file) => {
setProgress(0);
abortController.current = new AbortController();
try {
await upload(getSignedUrl, file, {
signal: abortController.current.signal,
onStart: (event) => {
console.log(`Starting upload: ${event.file.name}`);
},
onProgress: (event) => {
setProgress(event.progress);
},
onComplete: (event) => {
console.log("Upload complete:", event.response);
setProgress(0);
},
onError: (event) => {
if (event.type === "abort") {
console.log("Upload cancelled");
setProgress(0);
} else {
console.error("Upload failed:", event.error);
}
},
});
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
console.log("Upload aborted");
} else {
console.error("Upload failed:", error);
}
setProgress(0);
}
};
return (
<div>
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleUpload(file);
}}
/>
<button
onClick={() => {
abortController.current.abort();
setProgress(0);
}}
>
Cancel Upload
</button>
<progress value={progress} max="100" />
</div>
);
}
Multipart Upload Example
import { useApexxCloud } from "@apexxcloud/react";
import { useEffect, useRef } from "react";
import { useState } from "react";
export default function MultipartUploader() {
const { upload, uploadMultipart } = useApexxCloud();
const [progress, setProgress] = useState(0);
const abortController = useRef(new AbortController());
const getSignedUrl = async (type: string, params: any) => {
const response = await fetch(
`http://localhost:5000/api/files/get-signed-url/${type}`,
{
method: "POST",
body: JSON.stringify(params),
headers: {
"Content-Type": "application/json",
},
}
);
const data = await response.json();
return data.url;
};
const handleUpload = async (file) => {
setProgress(0);
abortController.current = new AbortController();
try {
// just replace upload with uploadMultipart
await uploadMultipart(getSignedUrl, file, {
signal: abortController.current.signal,
onStart: (event) => {
console.log(`Starting upload: ${event.file.name}`);
},
onProgress: (event) => {
setProgress(event.progress);
},
onComplete: (event) => {
console.log("Upload complete:", event.response);
setProgress(0);
},
onError: (event) => {
if (event.type === "abort") {
console.log("Upload cancelled");
setProgress(0);
} else {
console.error("Upload failed:", event.error);
}
},
});
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
console.log("Upload aborted");
} else {
console.error("Upload failed:", error);
}
setProgress(0);
}
};
return (
<div>
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleUpload(file);
}}
/>
<button
onClick={() => {
abortController.current.abort();
setProgress(0);
}}
>
Cancel Upload
</button>
<progress value={progress} max="100" />
</div>
);
}
Hook Return Value
| Property | Type | Description |
|---|---|---|
upload | (getSignedUrl: GetSignedUrlFn, file: File, options?: UploadOptions) => Promise<any> | Function for single file upload |
uploadMultipart | (getSignedUrl: GetSignedUrlFn, file: File, options?: MultipartUploadOptions) => Promise<any> | Function for multipart upload |
transformImage | TransformBuilder | Builder for image transformations |
transformVideo | VideoTransformBuilder | Builder for video transformations |
transformDocument | DocumentTransformBuilder | Builder for document transformations |
transformWithString | (path: string, transformations: string | string[]) => string | Direct transformation using strings |
Upload Options
| Option | Type | Description |
|---|---|---|
signal | AbortSignal | AbortSignal for cancelling upload |
onStart | (event: StartEvent) => void | Called when upload starts |
onProgress | (event: ProgressEvent) => void | Called during upload progress |
onComplete | (event: CompleteEvent) => void | Called when upload completes |
onError | (event: ErrorEvent) => void | Called if upload fails |
Multipart Upload Options
Includes all Upload Options plus:| Option | Type | Description |
|---|---|---|
partSize | number | Size of each part in bytes (default: 5MB) |
concurrency | number | Number of concurrent part uploads (default: 3) |
onPartComplete | (part: PartCompleteEvent) => void | Called when a part completes |
Backend Implementation
See the JavaScript SDK documentation for details on implementing the backend signed URL endpoint.File Uploader Component
The SDK provides a pre-builtFileUploader component that you can use out of the box:
import { FileUploader } from "@apexxcloud/react";
function App() {
const getSignedUrl = async (type, params) => {
// Your signed URL implementation
};
return (
<FileUploader
getSignedUrl={getSignedUrl}
onUploadComplete={(response) => console.log("Upload complete:", response)}
onUploadError={(error) => console.error("Upload failed:", error)}
multipart={false}
accept={{ "image/*": [".png", ".jpg", ".jpeg"] }}
maxSize={10} // MB
/>
);
}
Component Props
| Prop | Type | Description |
|---|---|---|
getSignedUrl | GetSignedUrlFn | Function to get signed URLs from your backend |
onUploadComplete | (response: any) => void | Called when upload completes successfully |
onUploadError | (error: Error) => void | Called when upload fails |
multipart | boolean | Enable multipart upload for large files |
accept | Record<string, string[]> | Accepted file types |
maxSize | number | Maximum file size in MB |
Custom Implementation
If you want to customize the uploader or build your own, here’s the complete implementation of our FileUploader component that you can use as a reference:import React, { useCallback, useState, useRef } from "react";
import { useDropzone } from "react-dropzone";
import { useApexxCloud } from "../hooks/useApexxCloud";
import {
GetSignedUrlFn,
ProgressEvent,
MultipartProgressEvent,
} from "@apexxcloud/sdk-js";
import clsx from "clsx";
import "./FileUploader.css";
interface FileUploaderProps {
getSignedUrl: GetSignedUrlFn;
onUploadComplete?: (response: any) => void;
onUploadError?: (error: Error) => void;
multipart?: boolean;
accept?: Record<string, string[]>;
maxSize?: number;
}
export const FileUploader: React.FC<FileUploaderProps> = ({
getSignedUrl,
onUploadComplete,
onUploadError,
multipart = false,
accept,
maxSize,
}) => {
const { upload, uploadMultipart } = useApexxCloud();
const [progress, setProgress] = useState(0);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const handleProgress = useCallback(
(event: ProgressEvent | MultipartProgressEvent) => {
setProgress(event.progress);
},
[]
);
const handleUpload = useCallback(
async (file: File) => {
setError(null);
setUploading(true);
setProgress(0);
abortControllerRef.current = new AbortController();
try {
const uploadFn = multipart ? uploadMultipart : upload;
const result = await uploadFn(getSignedUrl, file, {
onProgress: handleProgress,
onComplete: (event) => {
setProgress(100);
onUploadComplete?.(event.response);
},
onError: (event) => {
setError(event.error.message);
onUploadError?.(event.error);
},
signal: abortControllerRef.current.signal,
...(multipart && {
partSize: 5 * 1024 * 1024, // 5MB chunks
concurrency: 3,
}),
});
return result;
} catch (error) {
if (error instanceof Error) {
setError(error.message);
onUploadError?.(error);
}
} finally {
setUploading(false);
}
},
[
upload,
uploadMultipart,
getSignedUrl,
multipart,
onUploadComplete,
onUploadError,
]
);
const handleCancel = useCallback(() => {
abortControllerRef.current?.abort();
setUploading(false);
setProgress(0);
}, []);
const { getRootProps, getInputProps, isDragActive, fileRejections } =
useDropzone({
onDrop: useCallback(
(acceptedFiles: File[]) => {
const file = acceptedFiles[0];
if (file) handleUpload(file);
},
[handleUpload]
),
accept,
maxSize: maxSize ? maxSize * 1024 * 1024 : undefined,
multiple: false,
disabled: uploading,
});
const acceptedFileTypes = accept
? Object.values(accept).flat().join(", ")
: "All files";
const maxSizeFormatted = maxSize ? `${maxSize}MB` : "Unlimited";
return (
<div className={clsx("apexx-uploader-container")}>
<div
{...getRootProps()}
className={clsx(
"apexx-uploader-dropzone",
isDragActive && "active",
uploading && "disabled"
)}
>
<input {...getInputProps()} />
{uploading ? (
<div className="apexx-uploader-progress">
<div className="apexx-uploader-progress-bar">
<div
className="apexx-uploader-progress-fill"
style={{ width: `${progress}%` }}
/>
</div>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: "0.5rem",
}}
>
<span>{progress.toFixed(0)}% complete</span>
<button
onClick={(e) => {
e.stopPropagation();
handleCancel();
}}
className="apexx-uploader-cancel"
>
Cancel
</button>
</div>
</div>
) : (
<div className="apexx-uploader-content">
<svg
className="apexx-uploader-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<p>Drop files here or click to upload</p>
<small className="apexx-uploader-limits">
Accepted: {acceptedFileTypes}
<br />
Max size: {maxSizeFormatted}
</small>
</div>
)}
{fileRejections.length > 0 && (
<div className="apexx-uploader-error">
{fileRejections[0].errors
.map((err) =>
err.code === "file-too-large"
? `File is too large. Maximum size is ${maxSize}MB`
: err.message
)
.join(", ")}
</div>
)}
{error && <div className="apexx-uploader-error">{error}</div>}
</div>
</div>
);
};
.apexx-uploader-container {
width: 100%;
max-width: 42rem;
margin: 0 auto;
}
.apexx-uploader-dropzone {
padding: 1.5rem;
border: 2px dashed #e5e7eb;
border-radius: 0.5rem;
transition: all 0.2s ease;
background: white;
}
.apexx-uploader-dropzone.active {
border-color: #3b82f6;
background: #eff6ff;
}
.apexx-uploader-dropzone:not(.disabled):hover {
border-color: #9ca3af;
cursor: pointer;
}
.apexx-uploader-progress {
margin-top: 1rem;
}
.apexx-uploader-progress-bar {
position: relative;
width: 100%;
height: 0.5rem;
background: #e5e7eb;
border-radius: 9999px;
overflow: hidden;
}
.apexx-uploader-progress-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: #3b82f6;
transition: width 0.3s ease;
}
.apexx-uploader-content {
text-align: center;
}
.apexx-uploader-icon {
width: 3rem;
height: 3rem;
margin: 0 auto 1rem;
color: #9ca3af;
transition: color 0.2s ease;
}
.active .apexx-uploader-icon {
color: #3b82f6;
}
.apexx-uploader-error {
margin-top: 1rem;
padding: 0.75rem;
background: #fef2f2;
border-radius: 0.375rem;
color: #dc2626;
font-size: 0.875rem;
}
.apexx-uploader-cancel {
padding: 0.25rem 0.75rem;
color: #dc2626;
font-size: 0.875rem;
border-radius: 0.375rem;
transition: all 0.2s ease;
}
.apexx-uploader-cancel:hover {
background: #fef2f2;
color: #b91c1c;
}
.apexx-uploader-limits {
color: #666;
font-size: 0.875rem;
margin-top: 0.5rem;
}
.apexx-uploader-error {
color: #dc2626;
margin-top: 0.5rem;
font-size: 0.875rem;
}
- Drag and drop support
- Upload progress tracking
- Error handling
- File type and size validation
- Upload cancellation
- Responsive design
- Accessibility features
Requirements
- React 16.8+ (Hooks support)
- @apexxcloud/sdk-js as a peer dependency
Transformations
Image Transformations
import { useApexxCloud } from "@apexxcloud/react";
function ImageTransformExample() {
const { transformImage } = useApexxCloud();
// Basic transformations
const url = transformImage
.width(800)
.height(600)
.crop("fill")
.quality("auto:good")
.buildUrl("path/to/image.jpg");
// Chaining multiple transformations
const complexUrl = transformImage
.width(800)
.height(600)
.crop("fill")
.chain()
.blur(20)
.grayscale()
.buildUrl("path/to/image.jpg");
return (
<div>
<img src={url} alt="Transformed image" />
<img src={complexUrl} alt="Complex transformed image" />
</div>
);
}
Video Transformations
function VideoTransformExample() {
const { transformVideo } = useApexxCloud();
// Basic video transformation
const videoUrl = transformVideo
.width(1280)
.height(720)
.format("mp4")
.buildUrl("path/to/video.mp4");
// Generate video thumbnail
const thumbnailUrl = transformVideo
.thumbnail(5.0) // Seek to 5 seconds
.width(400)
.height(300)
.crop("fill")
.buildUrl("path/to/video.mp4");
return (
<div>
<video src={videoUrl} controls />
<img src={thumbnailUrl} alt="Video thumbnail" />
</div>
);
}
Document Transformations
function DocumentTransformExample() {
const { transformDocument } = useApexxCloud();
// Convert document format
const pdfUrl = transformDocument
.format("pdf")
.quality("auto:high")
.buildUrl("path/to/document.docx");
// Generate document thumbnail
const thumbnailUrl = transformDocument
.thumbnail(1) // First page
.width(800)
.height(1200)
.buildUrl("path/to/document.pdf");
return (
<div>
<iframe src={pdfUrl} />
<img src={thumbnailUrl} alt="Document thumbnail" />
</div>
);
}