A File is a value a server action can take. React’s Flight format carries it
as binary, so there is nothing to encode on the way out and nothing to decode
on the way in.
One file
"use client";
import { useState } from 'react';
import { uploadAvatar } from '../actions';
export function AvatarUpload() {
const [url, setUrl] = useState<string | null>(null);
async function submit(formData: FormData) {
const file = formData.get('avatar') as File;
if (!file || file.size === 0) return;
setUrl(await uploadAvatar(file));
}
return (
<form action={submit}>
<input type="file" name="avatar" accept="image/*" />
<button type="submit">Upload</button>
{url && <img src={url} alt="" />}
</form>
);
}The action receives the File itself — name, type, size and all:
'use server'
import { writeFile } from 'node:fs/promises';
export async function uploadAvatar(file: File): Promise<string> {
const bytes = new Uint8Array(await file.arrayBuffer());
await writeFile(`./public/avatars/${file.name}`, bytes);
return `/avatars/${file.name}`;
}Nothing about that is upload-specific: it is the same call, the same serialisation and the same round trip as any other server action.
The whole form at once
An action can take the FormData instead of individual arguments, which is
usually simpler when the form mixes files and fields:
"use client";
import { createPost } from '../actions';
export function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<textarea name="body" />
<input type="file" name="cover" accept="image/*" />
<button type="submit">Create</button>
</form>
);
}'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const cover = formData.get('cover') as File;
// …
}Several files
formData.getAll gives every file from a multiple input, and an array of
File serialises as readily as one:
const files = formData.getAll('photos') as File[];
await uploadPhotos(files);'use server'
export async function uploadPhotos(files: File[]) {
for (const file of files) {
// …
}
}Size
The whole body is buffered before the action runs — it is one POST, not a
stream — so an upload occupies memory for as long as it takes. Two consequences
worth planning for:
- Keep any limit your host imposes as low as the app actually needs.
- For genuinely large files, do not send them through an action at all. Have the action mint a pre-signed URL and let the browser upload straight to storage; the bytes never touch your server.