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-formNext, set your environment variables in .env.local:
NEXT_PUBLIC_UPLOADCARE_PUBLIC_KEY=your_public_key_hereTo import the uploader component, use the following syntax depending on your framework:
For 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 { useForm, Controller, useWatch } from 'react-hook-form';
import { FileUploaderRegular } from '@uploadcare/react-uploader/next';
import type {
OutputError,
OutputFileEntry,
OutputFileErrorType,
} from '@uploadcare/file-uploader';
import '@uploadcare/react-uploader/core.css';
const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'application/pdf'];
const MAX_FILE_SIZE = 1 * 1024 * 1024; // 1 MB
interface ProductFormData {
productName: string;
description: string;
productImageUrl: string;
}
export default function ProductUploadForm() {
const { control, handleSubmit, formState, setError } =
useForm<ProductFormData>({
defaultValues: {
productName: '',
description: '',
productImageUrl: '',
},
});
const productImageUrl = useWatch({
control,
name: 'productImageUrl',
});
// Keep required check in form state; Uploadcare handles upload UI errors.
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;
}, []);
const handleFileUploadSuccess = (
file: OutputFileEntry<'success'>,
fieldChange: (value: string) => void,
) => {
console.log('Uploaded file:', file);
const uuid = file.uuid;
let cdnUrl = file.cdnUrl;
console.log('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;
}
console.log('Setting productImageUrl to:', cdnUrl);
fieldChange(cdnUrl);
};
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' });
}
};
const handleFileUploadFailed = (event: OutputFileEntry<'failed'>) => {
const errors = event.errors as
| OutputError<OutputFileErrorType>[]
| undefined;
if (errors && errors.length > 0) {
setError('productImageUrl', {
type: 'manual',
message: errors[0].message || 'File upload 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}
accept={ALLOWED_FILE_TYPES.join(',')}
maxLocalFileSizeBytes={MAX_FILE_SIZE}
onFileUploadSuccess={(event) =>
handleFileUploadSuccess(event, onChange)
}
onFileUploadFailed={handleFileUploadFailed}
/>
{error && <p>{error.message}</p>}
</>
)}
/>
</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, and server-side submission — 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.