Skip to content

Commit 25a35de

Browse files
docs: file upload "how to" (#12241)
* First draft of file uploads guide * Cleanup the file-uploads doc * Update docs/framework/how-to/file-uploads.md Co-authored-by: Michael Jackson <[email protected]> * Update docs/framework/how-to/file-uploads.md Co-authored-by: Michael Jackson <[email protected]> * Change "file-storage.server" to "avatar-storage.server" --------- Co-authored-by: Michael Jackson <[email protected]>
1 parent b099df0 commit 25a35de

File tree

2 files changed

+195
-2
lines changed

2 files changed

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

docs/framework/how-to/resource-routes.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
title: Resource Routes
33
---
44

5-
<docs-info>This feature is available when using the [React Router Vite Plugin][vite-plugin]</docs-info>
6-
75
# Resource Routes
86

97
Resource Routes are not part of your application UI, but are still part of your application. They can send any kind of Response.

0 commit comments

Comments
 (0)