UNPKG

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