UNPKG

@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
# 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.