Reputation: 510
I want my Next.js/React app to allow a user to upload an image file to a storage.
My guess is that it is desirable to put all the storage handling part of the app to back end part of the app (to a server action) because some data like storage handling with credentials handling is better to stay away from front end and browser consoles.
When I try to pass a File object to my server action I get an error:
Error: Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported.
My code is two files page.tsx:
'use client'
import { uploadServerAction } from "./storageStuff";
import React, { ChangeEventHandler, MouseEventHandler, useState } from "react";
export default function Home() {
const [file, setFile] = useState<File | null>(null);
const handleFileChange: ChangeEventHandler<HTMLInputElement> = (e) => {
e.preventDefault();
setFile(e.target.files![0]);
};
const handleUpload: MouseEventHandler<HTMLButtonElement> = async (e) => {
e.preventDefault();
uploadServerAction(file)
}
return (
<>
<div>
<input type="file" onChange={handleFileChange} />
<button type="button" onClick={handleUpload}>
Upload Image
</button>
</div>
</>
);
}
and storageStuff.ts:
'use server'
export async function uploadServerAction(file: File | null) {
'use server'
...
}
So now I have a few questions:
I am a complete newbie in React/Next.js who have read a few chunks of official docs and some how-to's. So my code may have really stupid things.
Upvotes: 1
Views: 1068
Reputation: 1
Server actions do not allow transfer of blobs (files, images) directly. Only serializable arguments and return values are allowed. The React dev website outlines these in full detail.
WarlockJa's answer fixes the problem not because of using form action instead of an imperative server action call, but because the method uses FormData (allowed in the React dev website).
I experienced the OP's problem as well, but solved it by creating a new FormData object, setting a new key/value pair (formData.set('file', blob)) and invoking the server action imperatively. I suppose another way of doing this would be to create an ArrayBuffer (blob.arrayBuffer()) but I have not tried this.
Upvotes: 0
Reputation: 11
I had similar problem and the solution I found is to use form action instead.
"use client"
export default function UploadFile({ path }: { path: string }) {
const [error, action] = useFormState(writeFile.bind(null, path), {});
const formRef = useRef(null);
return (
<form action={action} ref={formRef}>
<Label htmlFor="file" className="cursor-pointer">
<span className="sr-only">upload file</span>
<UploadIcon />
</Label>
<Input type="file" id="file" name="file" />
{error?.file && <div className="text-destructive">{error.file}</div>}
<SubmitButton />
</form>
);
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<Button type="submit" disabled={pending}>
{pending ? "Saving..." : "Save"}
</Button>
);
}
And the form action is
"use server"
import fs from "fs/promises";
import { File } from "buffer";
import { z } from "zod";
import { revalidatePath } from "next/cache";
const fileSchema = z.object({
file: z
.instanceof(File, { message: "Required" })
.refine((file) => file.size > 0, "Required"),
});
export async function writeFile(
path: string,
prevState: unknown,
formData: FormData
) {
// validating form data
const result = fileSchema.safeParse(Object.fromEntries(formData.entries()));
if (result.success === false) {
return result.error.formErrors.fieldErrors;
}
const file = result.data.file;
await fs.writeFile(
`${path}/${file.name}`,
Buffer.from(await file.arrayBuffer())
);
revalidatePath("/");
}
Upvotes: 0