Next.js中使用Zod与React Hook Form上传图片至Supabase Storage并存储公共URL到表单的问题求助
Next.js中使用Zod与React Hook Form上传图片至Supabase Storage并存储公共URL到表单的问题求助
大家好,我最近在开发一个Next.js图书管理应用,遇到了图片上传相关的问题,折腾了好久没搞定,想请社区的朋友们帮忙看看。
我的需求
用户通过表单添加图书时,需要上传封面图到Supabase的book-pics存储桶,然后把图片的公共URL存入图书表的bookImageUrl字段,最后和其他表单数据一起提交保存。
我已经做的工作
- 用React Hook Form + Zod处理表单验证和状态
- 配置了Supabase Storage的
book-pics桶 - 写了单独的
UploadBookImage.tsx组件处理图片上传 - 表单提交逻辑已经对接了Supabase数据库的插入接口
遇到的问题
现在图片根本传不到book-pics桶里,表单提交时bookImageUrl字段也没有正确的URL值,导致数据库里这个字段是空的。
我的代码片段
UploadBookImage.tsx(图片上传组件)
import { createClient } from "../../../../../utils/supabase/client"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { useState } from "react"; export default function UploadBookImage({ onUpload, }: { size: number; url: string | null; onUpload: (url: string) => void; }) { const supabase = createClient(); const [uploading, setUploading] = useState(false); const uploadAvatar: React.ChangeEventHandler<HTMLInputElement> = async ( event ) => { try { setUploading(true); if (!event.target.files || event.target.files.length === 0) { throw new Error("You must select an image to upload."); } const file = event.target.files[0]; const fileExt = file.name.split(".").pop(); const filePath = `books/${Date.now()}.${fileExt}`; const { error: uploadError } = await supabase.storage .from("avatars") .upload(filePath, file); if (uploadError) { throw uploadError; } onUpload(filePath); } catch (error) { alert(`Error uploading avatar! ${error}`); } finally { setUploading(false); } }; return ( <div> <div className="grid w-full max-w-sm items-center gap-1.5"> <Label htmlFor="picture"> {uploading ? "Uploading ..." : "Upload"} </Label> <Input id="picture" type="file" accept="image/**" onChange={uploadAvatar} disabled={uploading} name="bookImageUrl" /> </div> </div> ); }
表单组件BookForm.tsx
const BookForm: React.FC<BookFormProps> = ({ authors }) => { const [state, action, pending] = useActionState(addBook, undefined); const [bookImageUrl, setBookImageUrl] = useState<string | null>(null); // React Hook Form with default values const form = useForm<BookInferSchema>({ resolver: zodResolver(BookSchema), defaultValues: { //rest of the values bookImageUrl: "", }, }); //submitting the forms async function onSubmit(data: BookInferSchema) { try { const formData = new FormData(); if (bookImageUrl) { data.bookImageUrl = bookImageUrl; // Attach uploaded image URL } Object.entries(data).forEach(([key, value]) => { formData.append( key, value instanceof Date ? value.toISOString() : value.toString() ); }); //sending the formData to the action.ts for submitting the forms const response = (await action(formData)) as { error?: string; message?: string; } | void; //Error or success messages for any submissions and any errors/success from the server if (response?.error) { toast({ title: "Error", description: `An error occurred: ${response.error}`, }); } else { form.reset(); } } catch { toast({ title: "Error", description: "An unexpected error occured.", }); } } return ( <Form {...form}> <form className="space-y-8" onSubmit={(e) => { e.preventDefault(); startTransition(() => { form.handleSubmit(onSubmit)(e); }); }} > <UploadBookImage size={150} url={bookImageUrl} onUpload={(url) => setBookImageUrl(url)} /> {/* rest of the input fields */} </form> </Form> ); }; export default BookForm;
服务端action.ts
"use server" export async function addBook(state: BookFormState, formData: FormData) { // Validate form fields // Log all form data to debug for (const pair of formData.entries()) { console.log(`${pair[0]}: ${pair[1]}`); } const validatedFields = BookSchema.safeParse({ //rest of the values bookImageUrl: formData.get("bookImageUrl"), }); // Check if validation failed if (!validatedFields.success) { console.error("Validation Errors:", validatedFields.error.format()); // Log errors return { errors: validatedFields.error.flatten().fieldErrors, }; } // Prepare for insertion into the new database const { ...restOfValues, bookImageUrl } = validatedFields.data // Insert the new author into the database const supabase = createClient(); const {data, error} = await supabase.from('books').insert({ ...restOfValues, bookImageUrl}); if(data){ console.log(data,"data in the addBook function") } if (error) { return { error: true, message: error.message, }; } return { error: false, message: 'Book updated successfully', }; }
数据库表定义
create table public.books ( -- rest of the columns "bookImageUrl" text null, constraint books_pkey primary key (isbn), constraint books_author_id_fkey foreign key (author_id) references authors (id) on delete cascade ) tablespace pg_default;
Storage桶的RLS策略
alter policy "Enable insert for authenticated users only" on "public"."books" to authenticated with check ( true );
Zod Schema
import { z } from "zod"; export const BookSchema = z.object({ //rest of the values bookImageUrl :z.string().optional() }); // TypeScript Type for Book export type BookInferSchema = z.infer<typeof BookSchema>; //Form state for adding and editing books export type BookFormState = | { errors?: { //rest of the values bookImageUrl?: string[]; }; message?: string; } | undefined;
我自己排查的方向
我怀疑是存储桶的名称写错了?或者是没有正确获取公共URL?还有表单状态和上传组件的联动是不是有问题?
希望大家能帮我找出问题所在,给出修复建议,谢谢啦!
备注:内容来源于stack exchange,提问作者JS3
相关产品推荐
相关产品推荐

