Home/Blog/How do I use Uploadcare with React Hook Form?

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-form

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 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 onChange doesn’t fire immediately
    Cause: The CDN URL arrives asynchronously in the onSuccess callback
    Fix: Explicitly call the onChange function passed from Controller in your callback, not in useEffect

  • Problem: Form validation triggers before file upload completes
    Cause: The fileUrl field may be empty when the user clicks submit
    Fix: Add rules: { 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 from FileUploaderRegular may be incompatible with Controller’s expected type
    Fix: Cast the onChange parameter to (value: string) => void or 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.

Further reading

Build file handling in minutesStart for free

Ready to get started?

Join developers who use Uploadcare to build file handling quickly and reliably.

Sign up for free