How do I use Uploadcare with React Hook Form?
To integrate Uploadcare with React Hook Form, wrap FileUploaderRegular with the Controller component
and set the form value to the CDN URL on successful upload.
This requires importing Controller from react-hook-form and configuring validation rules.
Set up the Uploadcare File Uploader
Install Uploadcare and React Hook Form:
npm install @uploadcare/react-uploader react-hook-formFor Next.js:
import { FileUploaderRegular } from '@uploadcare/react-uploader/next';For standard React:
import { FileUploaderRegular } from '@uploadcare/react-uploader';Also import Controller from react-hook-form:
import { useForm, Controller } from 'react-hook-form';Wrap FileUploaderRegular with Controller
Here’s a simple form with file upload and other fields controlled together:
'use client';
import { useForm, Controller } from 'react-hook-form';
import { FileUploaderRegular } from '@uploadcare/react-uploader/next';
import '@uploadcare/react-uploader/core.css';
interface FormData {
fileName: string;
email: string;
fileUrl: string;
}
export default function UploadWithHookForm() {
const { control, handleSubmit, watch, formState } = useForm<FormData>({
defaultValues: {
fileName: '',
email: '',
fileUrl: '',
},
});
const fileUrl = watch('fileUrl');
const handleFileUploadSuccess = (
event: unknown,
fieldChange: (value: string) => void,
) => {
let cdnUrl: string | undefined;
if (event && typeof event === 'object') {
const eventObj = event as {
detail?: { cdnUrl?: string };
cdnUrl?: string;
};
cdnUrl = eventObj.detail?.cdnUrl || eventObj.cdnUrl;
}
if (typeof cdnUrl === 'string') {
fieldChange(cdnUrl);
}
};
const onSubmit = async (data: FormData) => {
try {
console.log('Form submitted:', data);
// Send data to API: { fileName, email, fileUrl }
const response = await fetch('/api/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('Response:', result);
} catch (error) {
console.error('Error:', error);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div style={{ marginBottom: "20px" }}>
<label htmlFor="fileName">File Name:</label>
<Controller
name="fileName"
control={control}
rules={{ required: 'File name is required' }}
render={({ field, fieldState: { error } }) => (
<>
<input
{...field}
type="text"
id="fileName"
placeholder="Enter file name"
/>
{error && <p>{error.message}</p>}
</>
)}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label htmlFor="email">Email:</label>
<Controller
name="email"
control={control}
rules={{
required: 'Email is required',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Invalid email',
},
}}
render={({ field, fieldState: { error } }) => (
<>
<input
{...field}
type="email"
id="email"
placeholder="user@example.com"
/>
{error && <p>{error.message}</p>}
</>
)}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label>Upload File:</label>
<Controller
name="fileUrl"
control={control}
rules={{ required: 'Please upload a file' }}
render={({ field: { onChange }, fieldState: { error } }) => (
<>
<FileUploaderRegular
pubkey={process.env.NEXT_PUBLIC_UPLOADCARE_PUBLIC_KEY as string}
onFileUploadSuccess={event =>
handleFileUploadSuccess(event, onChange)
}
/>
{error && <p>{error.message}</p>}
{fileUrl && <p>✓ File uploaded</p>}
</>
)}
/>
</div>
<button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}This component integrates file upload into a form with validation, for production you’ll need custom validators, error handling, and server-side verification of the CDN URL.
Add file type validation and error handling
Here’s a production-ready component with validation rules, file type checking, error messages, and server-side submission:
'use client';
import { useCallback } from 'react';
import Image from 'next/image';
import { useForm, Controller } from 'react-hook-form';
import { FileUploaderRegular } from '@uploadcare/react-uploader/next';
import '@uploadcare/react-uploader/core.css';
const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'application/pdf'];
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
interface ProductFormData {
productName: string;
description: string;
productImage: null;
productImageUrl: string;
}
export default function ProductUploadForm() {
const { control, handleSubmit, watch, formState, setError } =
useForm<ProductFormData>({
defaultValues: {
productName: '',
description: '',
productImage: null,
productImageUrl: '',
},
});
const productImageUrl = watch('productImageUrl');
// Custom validator for file type and size
const validateFileUpload = useCallback(async (value: string) => {
if (!value) {
return 'File is required';
}
// Value is the CDN URL; in production, verify with your server
return true;
}, []);
type UploadcareFileEntry = {
cdnUrl?: string;
uuid?: string;
size?: number;
mimeType?: string;
};
const handleFileUploadSuccess = (
file: UploadcareFileEntry,
fieldChange: (value: string) => void,
) => {
console.log('Uploaded file:', file);
const fileSize = file.size;
const mimeType = file.mimeType;
const uuid = file.uuid;
let cdnUrl = file.cdnUrl;
console.log('FileSize:', fileSize, 'MimeType:', mimeType, 'UUID:', uuid);
console.log('CdnUrl from file:', cdnUrl);
// Construct CDN URL if not provided
if (!cdnUrl && uuid) {
cdnUrl = `https://ucarecdn.com/${uuid}/`;
console.log('Constructed CDN URL:', cdnUrl);
}
if (!cdnUrl) {
console.warn('Could not find or construct cdnUrl');
return;
}
// Client-side validation
if (mimeType && !ALLOWED_FILE_TYPES.includes(mimeType)) {
setError('productImage', {
type: 'manual',
message: 'Only JPEG, PNG, and PDF files allowed',
});
return;
}
if (fileSize && fileSize > MAX_FILE_SIZE) {
setError('productImage', {
type: 'manual',
message: 'File size must be less than 10MB',
});
return;
}
console.log('Setting productImageUrl to:', cdnUrl);
fieldChange(cdnUrl);
};
const handleFileUploadFailed = (event: unknown) => {
let errors: Array<{ message: string }> | undefined;
if (event && typeof event === 'object' && 'detail' in event) {
const detail = (event as { detail?: Record<string, unknown> }).detail;
if (detail && typeof detail === 'object') {
errors = detail.errors as Array<{ message: string }> | undefined;
}
}
if (errors && errors.length > 0) {
setError('productImage', {
type: 'manual',
message: errors[0].message || 'File upload failed',
});
}
};
const onSubmit = async (data: ProductFormData) => {
try {
// Verify CDN URL on server before processing
const response = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: data.productName,
description: data.description,
imageUrl: data.productImageUrl,
}),
});
if (!response.ok) {
const error = await response.json();
setError('root', { type: 'manual', message: error.message });
return;
}
const result = await response.json();
console.log('Product created:', result);
// Reset form or redirect
} catch (err) {
console.error('Submission error:', err);
setError('root', { type: 'manual', message: 'Submission failed' });
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{formState.errors.root && <div>{formState.errors.root.message}</div>}
<div>
<label htmlFor="productName">Product Name:</label>
<Controller
name="productName"
control={control}
rules={{ required: 'Product name is required' }}
render={({ field, fieldState: { error } }) => (
<>
<input
{...field}
type="text"
id="productName"
placeholder="Enter product name"
/>
{error && <p>{error.message}</p>}
</>
)}
/>
</div>
<div>
<label htmlFor="description">Description:</label>
<Controller
name="description"
control={control}
rules={{ required: 'Description is required' }}
render={({ field, fieldState: { error } }) => (
<>
<textarea
{...field}
id="description"
placeholder="Enter product description"
rows={4}
/>
{error && <p>{error.message}</p>}
</>
)}
/>
</div>
<div>
<label>Product Image:</label>
<Controller
name="productImageUrl"
control={control}
rules={{ validate: validateFileUpload }}
render={({ field: { onChange }, fieldState: { error } }) => (
<>
<FileUploaderRegular
pubkey={process.env.NEXT_PUBLIC_UPLOADCARE_PUBLIC_KEY as string}
onFileUploadSuccess={event =>
handleFileUploadSuccess(event, onChange)
}
onFileUploadFailed={handleFileUploadFailed}
/>
{error && <p>{error.message}</p>}
{productImageUrl && (
<div>
<p>✓ Image uploaded</p>
<Image
src={productImageUrl}
alt="Product preview"
width={200}
height={200}
/>
</div>
)}
</>
)}
/>
</div>
<button
type="submit"
disabled={formState.isSubmitting || !productImageUrl}
>
{formState.isSubmitting ? 'Submitting...' : 'Create Product'}
</button>
</form>
);
}This production component includes file type validation, file size checks, error mapping, server-side submission, and image preview — validating the CDN URL on the server ensures the file exists before processing.
Common errors and solutions
-
Problem: Controller
onChangedoesn’t fire immediately
Cause: The CDN URL arrives asynchronously in the onSuccess callback
Fix: Explicitly call theonChangefunction passed from Controller in your callback, not inuseEffect -
Problem: Form validation triggers before file upload completes
Cause: ThefileUrlfield may be empty when the user clicks submit
Fix: Addrules: { required: 'File required' }to the Controller and disable the submit button until the URL is set -
Problem: TypeScript errors with Controller value
Cause: The value type fromFileUploaderRegularmay be incompatible with Controller’s expected type
Fix: Cast the onChange parameter to(value: string) => voidor use a wrapper function that normalizes the input
Next steps
Explore adding image transformations through Uploadcare’s CDN URL API for on-the-fly cropping and resizing. For other ways to wire up the uploader in React, the React file upload guide covers additional component patterns, and Introducing the Uploadcare React file uploader explains the architecture behind the component.