Skip to content

File uploads

Sending files through a server action without encoding them.

Updated View as Markdown

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

src/components/AvatarUpload.tsxtsx
"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:

src/actions.tsts
'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>
  );
}
src/actions.tsts
'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);
src/actions.tsts
'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.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close