File uploads with Remix
As of writing this there is an official guide on file uploads in Remix but it's basically a placeholder until they iron out some APIs and have time to revisit the guide.
This is my unofficial guide.
<Form />
To use the Remix <Form />
component with files you must use the encType
prop and update the method
to "post" or "put":
<Form method='post' encType='multipart/form-data'>
<input type='file' name='my-file' />
<button>Submit</button>
</Form>
Now, in your route's action
you can pull that File
out of the form data.
export async function action({request}: ActionFuntionArgs) {
const formData = await request.formData()
const file = formData.get('my-file')
if (!(file instanceof File)) throw new Error('Invalid file')
// process file
return {
message: "File received"
}
}
The uploaded file(s) will be stored in memory and you'll need to decide what to do with them.
Base64
You could convert the upload into a base64 value:
const formData = await request.formData()
const file = formData.get('my-file')
if (!(file instanceof File)) throw new Error('Invalid file')
const arrayBuffer = await file.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
const base64 = buffer.toString('base64')
But most likely you'll want to store them somewhere...
Local file system
Luckily, Remix has a handy utility for storing uploads locally. Here's what that looks like:
import {
unstable_createFileUploadHandler,
unstable_parseMultipartFormData
} from '@remix-run/node'
export async function action({request}: ActionFuntionArgs) {
const uploadHandler = unstable_createFileUploadHandler({
// where to save the file
directory: '/app/storage',
// use the incoming filename instead of creating a new name
file: (args) => args.filename
})
const formData = await unstable_parseMultipartFormData(request, uploadHandler)
return {
message: "File received"
}
}
unstable_parseMultipartFormData
You might have noticed that request.formData()
went away and was replaced with this unstable_parseMultipartFormData
function. This is a Remix utility for iteratively deciding where uploads go.
request.formData()
- read everything into memory and then decide what to do with file dataunstable_parseMultipartFormData
- decide what to do with file data as it's being read
This allows users to upload big files without the application storing those entirely in memory all at once. Instead, you can stream those files into their destinations.
The unstable_parseMultipartFormData
function will iterate over the form data keys and if the key contains a file, it will hand that off to be processed by the upload handler.
Gotcha
One "gotcha" with parsing the form data this way is that you don't have access to the other form values because the FormData
instance hasn't been created yet. For example, say you had another input in the same form for the user to select a category for the file that determines where that file lives on disk of if you need to do some validation on the provided category.
const formData = await unstable_parseMultipartFormData(request, uploadHandler)
const category = formData.get('category')
// Now I just need to valida....oh wait, my file has already been uploaded...
To get around this, you can use the useSubmit
hook and append values as search params to be read in the action
.
const submit = useSubmit()
return (
<Form method="post" onSubmit={(e) => {
const formData = new FormData(e.currentTarget)
const params = new URLSearchParams({category: formData.get('category')})
const options = {
action: `/app/upload?${params}`,
method: "post",
encType: "multipart/form-data"
}
submit(formData, options)
}}>
)
And then in your action:
export async function action({request}: ActionFuntionArgs) {
const url = new URL(request.url)
const cateogry = url.searchParams.get('category')
// continue on to use unstable_parseMultipartFormData()
}
Custom upload handlers
Remix allows you to create your very own upload handlers.
const uploadHandler: UploadHandler = async ({ data, filename }) => {
// handle the data and filename however you need to
// return File | string | null | undefined
return "https://example.com/assets/newly-uploaded-file.png";
}
const formData = await unstable_parseMultipartFormData(request, uploadHandler);
// Remix swaps in whatever you return from your upload handler
const uploadUrl = formData.get('my-file')
Composing upload handlers
Remix also comes with a utility for using multiple upload handlers to handle certain uploads differently. The following is lifted directly from the Remix docs.
const uploadHandler = unstable_composeUploadHandlers(
// custom upload handler
async ({ name, contentType, data, filename }) => {
if (name !== "img") {
return undefined;
}
const uploadedImage = await uploadImageToCloudinary(data);
return uploadedImage.secure_url;
},
// fallback to memory for everything else
unstable_createMemoryUploadHandler()
);
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
);
In this example, the first handler only applies to the upload coming from the input with the name of "img":
<input type='file' name='img' />
By returning undefined
, Remix will try the next upload handler. In this case, it's the unstable_createMemoryUploadHandler
.
unstable_createMemoryUploadHandler
Wait, why is there a unstable_createMemoryUploadHandler
? Wouldn't that be the same as request.formData()
? Yep, almost. This utility does come with the ability to limit the upload size. It defaults to 3MB.
const uploadHandler = unstable_createMemoryUploadHandler({maxPartSize: 5000000}) // 5MB
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
);
const file = formData.get('my-file')
// up to you to handle now
Upload to S3
Here's an example that illustrates efficiently streaming the upload to S3.
import {
type ActionFunctionArgs,
json,
unstable_parseMultipartFormData,
type UploadHandler,
} from "@remix-run/node";
import { S3Client } from "@aws-sdk/client-s3";
import { env } from "~/env.server";
import { Upload } from "@aws-sdk/lib-storage";
import { Readable } from "stream";
// you probably wouldn't actually create the s3 client here...
const s3 = new S3Client({
region: env.AWS_REGION,
credentials: {
accessKeyId: env.AWS_ACCESS_KEY_ID,
secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
},
});
export async function action({ request }: ActionFunctionArgs) {
const uploadHandler: UploadHandler = async ({
data,
filename,
}) => {
const readableStream = Readable.from(data);
const upload = new Upload({
client: s3,
params: {
Bucket: env.AWS_S3_BUCKET,
Key: filename,
Body: readableStream,
},
});
await upload.done();
return filename;
};
await unstable_parseMultipartFormData(request, uploadHandler);
return json({
success: true,
});
}
Hope this helps!