Better Upload

Form

It's common for file uploads to be part of a form. In this guide, we'll take a look at how to add file uploads to a form built using shadcn/ui. This guide will assume you have a basic understanding of:

This guide is for uploading multiple files, but the same principles apply for single file uploads.

Form with upload dropzone

Installation

To follow along with this guide, install the following shadcn/ui components:

npx shadcn@latest add form input button

This will also automatically install all required dependencies.

Set up upload route

Set up your upload route. Use your preferred framework, but for this example, we'll use Next.js.

app/api/upload/route.ts
import { S3Client } from '@aws-sdk/client-s3';
import { createUploadRouteHandler, route } from 'better-upload/server';
 
const s3 = new S3Client();
 
export const { POST } = createUploadRouteHandler({
  client: s3,
  bucketName: 'your-bucket-name',
  routes: {
    form: route({
      multipleFiles: true,
      maxFiles: 5,
      onBeforeUpload() {
        return {
          generateObjectKey: () => `form/${crypto.randomUUID()}`,
        };
      },
    }),
  },
});

We create the upload route form for multiple files. All files will have the same prefix form/ in the S3 bucket.

Define the form schema

We'll now create the form. The form uses the Upload Dropzone component to allow users to upload files. The form also has an input field for arbitrary text.

Define the form schema using zod. The schema contains two fields:

  • folderName: For the arbitrary text input.
  • objectKeys: For the uploaded files, stores the S3 object keys.
components/my-form.tsx
'use client'; // for next.js
 
import { z } from 'zod';
 
const formSchema = z.object({
  folderName: z.string().min(1),
  objectKeys: z.array(z.string()).min(1),
});

Define the form

Use the useForm hook from react-hook-form to create the form.

Also use the useUploadFiles hook to handle the file uploads.

'use client';
 
import { zodResolver } from '@hookform/resolvers/zod';
import { useUploadFiles } from 'better-upload/client';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
 
const formSchema = z.object({
  folderName: z.string().min(1),
  objectKeys: z.array(z.string()).min(1),
});
 
export function MyForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      folderName: '',
      objectKeys: [],
    },
  });
 
  const { control: uploadControl } = useUploadFiles({
    route: 'form',
    onUploadComplete: ({ files }) => {
      form.setValue(
        'objectKeys',
        files.map((file) => file.objectKey)
      );
    },
    onError: (error) => {
      form.setError('objectKeys', {
        message: error.message || 'An error occurred',
      });
    },
  });
 
  function onSubmit(data: z.infer<typeof formSchema>) {
    // call your API here
    console.log(data);
  }
}

Feel free to make changes to the code to suit your needs, such as calling your API on onSubmit.

Build the form UI

We can now use the <Form /> component from shadcn/ui to build our form.

'use client';
 
import { zodResolver } from '@hookform/resolvers/zod';
import { useUploadFiles } from 'better-upload/client';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
 
import { Button } from '@/components/ui/button';
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { UploadDropzone } from '@/components/ui/upload-dropzone';
 
const formSchema = z.object({
  folderName: z.string().min(1),
  objectKeys: z.array(z.string()).min(1),
});
 
export function MyForm() {
  // ...
 
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="folderName"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Folder name</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
 
        <FormField
          control={form.control}
          name="objectKeys"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Files</FormLabel>
              <FormControl>
                <UploadDropzone control={uploadControl} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
 
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  );
}

Optional: Hide dropzone after upload

Now let's hide the dropzone after the user has uploaded files. We can do this by using the uploadedFiles array returned by the useUploadFiles hook.

'use client';
 
export function MyForm() {
  const { control: uploadControl, uploadedFiles } = useUploadFiles({
    route: 'form',
    onUploadComplete: ({ files }) => {
      form.setValue(
        'objectKeys',
        files.map((file) => file.objectKey)
      );
    },
    onError: (error) => {
      form.setError('objectKeys', {
        message: error.message || 'An error occurred',
      });
    },
  });
 
  // ...
 
  return (
    <Form>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        {/* ... */}
 
        {uploadedFiles.length > 0 ? (
          <div className="flex flex-col">
            {uploadedFiles.map((file) => (
              <p key={file.objectKey}>{file.name}</p>
            ))}
          </div>
        ) : (
          <FormField
            control={form.control}
            name="objectKeys"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Files</FormLabel>
                <FormControl>
                  <UploadDropzone control={uploadControl} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
        )}
 
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  );
}

Advanced: Upload on form submit

In this example, we only upload the files after the user clicks on the submit button. We use the uploadOverride prop to override the default behavior of the <UploadDropzone />.

The full code example for the form is below.

components/my-form.tsx
'use client';
 
import { zodResolver } from '@hookform/resolvers/zod';
import { useUploadFiles } from 'better-upload/client';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
 
import { Button } from '@/components/ui/button';
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { UploadDropzone } from './ui/upload-dropzone';
 
const formSchema = z.object({
  folderName: z.string().min(1),
  files: z.array(z.instanceof(File)).min(1), // for Zod v4: z.array(z.file()).min(1),
});
 
export function FormUploader() {
  const {
    upload,
    control: uploadControl,
    isPending: isUploading,
  } = useUploadFiles({
    route: 'form',
    onError: (error) => {
      form.setError('files', {
        message: error.message || 'An error occurred',
      });
    },
  });
 
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      folderName: '',
      files: [],
    },
  });
 
  async function onSubmit(data: z.infer<typeof formSchema>) {
    const { files } = await upload(data.files);
 
    // call your API here
    console.log({
      folderName: data.folderName,
      objectKeys: files.map((file) => file.objectKey),
    });
  }
 
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="folderName"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Folder name</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
 
        {form.watch('files').length > 0 ? (
          <div className="flex flex-col">
            {form.watch('files').map((file, i) => (
              <p key={i}>{file.name}</p>
            ))}
          </div>
        ) : (
          <FormField
            control={form.control}
            name="files"
            render={() => (
              <FormItem>
                <FormLabel>Files</FormLabel>
                <FormControl>
                  <UploadDropzone
                    control={uploadControl}
                    uploadOverride={(files) => {
                      form.setValue('files', Array.from(files));
                    }}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
        )}
 
        <Button type="submit" disabled={isUploading}>
          Submit
        </Button>
      </form>
    </Form>
  );
}

On this page