UNPKG

5.58 kB Markdown View Raw
1---
2title: File Uploads
3---
4
5# File Uploads
6
7[MODES: framework]
8
9_Thank you to David Adams for [writing an original guide](https://programmingarehard.com/2024/09/06/remix-file-uploads-updated.html/) on which this doc is based. You can refer to it for even more examples._
10
11## Basic File Upload
12
13### 1. Setup some routes
14
15You can setup your routes however you like. This example uses the following structure:
16
17```ts filename=routes.ts
18import {
19 type RouteConfig,
20 route,
21} from "@react-router/dev/routes";
22
23export default [
24 // ... other routes
25 route("user/:id", "pages/user-profile.tsx", [
26 route("avatar", "api/avatar.tsx"),
27 ]),
28] satisfies RouteConfig;
29```
30
31### 2. Add the form data parser
32
33`form-data-parser` is a wrapper around `request.formData()` that provides streaming support for handling file uploads.
34
35```shellscript
36npm i @remix-run/form-data-parser
37```
38
39[See the `form-data-parser` docs for more information][form-data-parser]
40
41### 3. Create a route with an upload action
42
43The `parseFormData` function takes an `uploadHandler` function as an argument. This function will be called for each file upload in the form.
44
45<docs-warning>
46
47You must set the form's `enctype` to `multipart/form-data` for file uploads to work.
48
49</docs-warning>
50
51```tsx filename=pages/user-profile.tsx
52import {
53 type FileUpload,
54 parseFormData,
55} from "@remix-run/form-data-parser";
56import type { Route } from "./+types/user-profile";
57
58export async function action({
59 request,
60}: Route.ActionArgs) {
61 const uploadHandler = async (fileUpload: FileUpload) => {
62 if (fileUpload.fieldName === "avatar") {
63 // process the upload and return a File
64 }
65 };
66
67 const formData = await parseFormData(
68 request,
69 uploadHandler,
70 );
71 // 'avatar' has already been processed at this point
72 const file = formData.get("avatar");
73}
74
75export default function Component() {
76 return (
77 <form method="post" encType="multipart/form-data">
78 <input type="file" name="avatar" />
79 <button>Submit</button>
80 </form>
81 );
82}
83```
84
85## Local Storage Implementation
86
87### 1. Add the storage package
88
89`file-storage` is a key/value interface for storing [File objects][file] in JavaScript. Similar to how `localStorage` allows you to store key/value pairs of strings in the browser, file-storage allows you to store key/value pairs of files on the server.
90
91```shellscript
92npm i @remix-run/file-storage
93```
94
95[See the `file-storage` docs for more information][file-storage]
96
97### 2. Create a storage configuration
98
99Create a file that exports a `LocalFileStorage` instance to be used by different routes.
100
101```ts filename=avatar-storage.server.ts
102import { LocalFileStorage } from "@remix-run/file-storage/local";
103
104export const fileStorage = new LocalFileStorage(
105 "./uploads/avatars",
106);
107
108export function getStorageKey(userId: string) {
109 return `user-${userId}-avatar`;
110}
111```
112
113### 3. Implement the upload handler
114
115Update the form's `action` to store files in the `fileStorage` instance.
116
117```tsx filename=pages/user-profile.tsx
118import {
119 type FileUpload,
120 parseFormData,
121} from "@remix-run/form-data-parser";
122import {
123 fileStorage,
124 getStorageKey,
125} from "~/avatar-storage.server";
126import type { Route } from "./+types/user-profile";
127
128export async function action({
129 request,
130 params,
131}: Route.ActionArgs) {
132 async function uploadHandler(fileUpload: FileUpload) {
133 if (
134 fileUpload.fieldName === "avatar" &&
135 fileUpload.type.startsWith("image/")
136 ) {
137 let storageKey = getStorageKey(params.id);
138
139 // FileUpload objects are not meant to stick around for very long (they are
140 // streaming data from the request.body); store them as soon as possible.
141 await fileStorage.set(storageKey, fileUpload);
142
143 // Return a File for the FormData object. This is a LazyFile that knows how
144 // to access the file's content if needed (using e.g. file.stream()) but
145 // waits until it is requested to actually read anything.
146 return fileStorage.get(storageKey);
147 }
148 }
149
150 const formData = await parseFormData(
151 request,
152 uploadHandler,
153 );
154}
155
156export default function UserPage({
157 actionData,
158 params,
159}: Route.ComponentProps) {
160 return (
161 <div>
162 <h1>User {params.id}</h1>
163 <form
164 method="post"
165 // The form's enctype must be set to "multipart/form-data" for file uploads
166 encType="multipart/form-data"
167 >
168 <input type="file" name="avatar" accept="image/*" />
169 <button>Submit</button>
170 </form>
171
172 <img
173 src={`/user/${params.id}/avatar`}
174 alt="user avatar"
175 />
176 </div>
177 );
178}
179```
180
181### 4. Add a route to serve the uploaded file
182
183Create a [resource route][resource-route] that streams the file as a response.
184
185```tsx filename=api/avatar.tsx
186import {
187 fileStorage,
188 getStorageKey,
189} from "~/avatar-storage.server";
190import type { Route } from "./+types/avatar";
191
192export async function loader({ params }: Route.LoaderArgs) {
193 const storageKey = getStorageKey(params.id);
194 const file = await fileStorage.get(storageKey);
195
196 if (!file) {
197 throw new Response("User avatar not found", {
198 status: 404,
199 });
200 }
201
202 return new Response(file.stream(), {
203 headers: {
204 "Content-Type": file.type,
205 "Content-Disposition": `attachment; filename=${file.name}`,
206 },
207 });
208}
209```
210
211[form-data-parser]: https://www.npmjs.com/package/@remix-run/form-data-parser
212[file-storage]: https://www.npmjs.com/package/@remix-run/file-storage
213[file]: https://developer.mozilla.org/en-US/docs/Web/API/File
214[resource-route]: ../how-to/resource-routes
215
\No newline at end of file