@rsc-kit/mcp
Version:
An MCP server over what an rsc-kit build decided: the routes, why each one is static or not, and what it costs the browser.
128 lines (91 loc) • 3.24 kB
Markdown
# File uploads
> Sending files through a server action without encoding them.
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
```tsx title="src/components/AvatarUpload.tsx"
"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:
```ts title="src/actions.ts"
'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](/guides/server-actions).
## 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:
```tsx
"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>
);
}
```
```ts title="src/actions.ts"
'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:
```tsx
const files = formData.getAll('photos') as File[];
await uploadPhotos(files);
```
```ts title="src/actions.ts"
'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. The server
refuses a body over **8 MB** with a 413 before it holds a byte of it, and
the form reports that as a failed submission. Raise the ceiling when the app
genuinely uploads more:
```ts title="vite.config.ts"
rscKit({ maxActionBody: 32 * 1024 * 1024 })
```
Two consequences worth planning for:
- Keep the ceiling — and any limit a proxy in front 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.