Vyhledávání na webu

javascript - Problémy s nahráváním obrázků a validací formulářů v Next.js 14 se Shadcn, Zod a React Hook Form


Momentálně pracuji na aplikaci Next.js 14 s využitím Shadcn, Zod a React Hook Form a čelím několika problémům s nahráváním obrázků a ověřováním formuláře. Mám formulář pro vytvoření uživatelského profilu a používám funkci createProfile k odeslání dat z formuláře včetně obrázku. Nicméně se setkávám s následujícími problémy:

  • Problém s nahráváním obrázků - Django backend odpovídá chybovou zprávou: "Odeslaná data nebyla souborem. Zkontrolujte typ kódování ve formuláři.".
  • Problém s ID uživatele - Při odesílání formuláře hlásí backend Django chybu související s polem „user“: „Neplatný pk „0“ - objekt neexistuje.“ Domnívám se, že to souvisí s odesíláním řetězce místo čísla do backendu Django.
  • Problém s komentovaným vstupním polem - Zakomentoval jsem alternativní způsob, jak zpracovat vstupní pole pro obrázky ve svém kódu. Když použiji přístup {...field} u tohoto pole, celá moje aplikace spadne při nahrávání obrázků. Nejsem si jistý, proč k tomu dochází, a ocenil bych jakékoliv názory na tento problém. Zde je můj kód (omlouvám se, jestli je příliš velký, myslel jsem si, že je lepší ukázat celý) ProfileForm.tsx
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";

import { useGlobalContext } from "@/components/context/GlobalContext";

import { toast } from "@/lib/use-toast";
import { createProfile } from "@/lib/users/createProfile";

import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

// https://github.com/colinhacks/zod/issues/387
// https://github.com/blitz-js/blitz/discussions/2292#discussioncomment-826778

const profileFormSchema = z.object({
  full_name: z.string().min(2).max(20),
  address: z.string().min(2).max(50),
  phone_number: z.string().min(2).max(20),
  picture: z
    .any()
    .refine((file) => file?.length == 1, "File is required.")
    .refine(
      (file) =>
        file[0]?.type === "image/png" ||
        file[0]?.type === "image/jpeg" ||
        file[0]?.type === "image/jpg",
      "Must be a png, jpeg or jpg.",
    )
    .refine((file) => file[0]?.size <= 5000000, `Max file size is 5MB.`),
  // picture: typeof window === "undefined" ? z.any() : z.instanceof(File),
});

export type ProfileFormSchemaType = z.infer<typeof profileFormSchema>;

export default function ProfileForm() {
  const { userInfo, setUserInfo, accessToken } = useGlobalContext();

  const form = useForm<ProfileFormSchemaType>({
    resolver: zodResolver(profileFormSchema),
    defaultValues: {
      full_name: "",
      address: "",
      phone_number: "",
      picture: undefined,
    },
  });

  async function onSubmit(values: ProfileFormSchemaType) {
    const formData = new FormData();

    formData.append("picture", values.picture);
    formData.append("full_name", values.full_name);
    formData.append("address", values.address);
    formData.append("phone_number", values.phone_number);
    formData.append("user", String(userInfo.id));

    console.log(formData);

    try {
      const response = await createProfile(formData, accessToken);
      console.log(response);

      toast({
        title: "success",
      });
    } catch (error) {
      toast({
        title: "error",
        description: error?.toString(),
      });
      console.error(error);
    }
  }

  const fileRef = form.register("picture", { required: true });

  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit)}
        className="space-y-8 rounded-tr-2xl rounded-tl-2xl border p-8"
      >
        <FormField
          control={form.control}
          name="full_name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Full name</FormLabel>
              <FormControl>
                <Input placeholder="John Doe" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="phone_number"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Phone number</FormLabel>
              <FormControl>
                <Input placeholder="+38269123123" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="address"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Address</FormLabel>
              <FormControl>
                <Input placeholder="Podgorica, Donja Gorica" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        {/* <FormField
          control={form.control}
          name="picture"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Picture</FormLabel>
              <FormControl>
                <Input
                  accept="image/png, image/jpeg, image/jpg"
                  type="file"
                  placeholder="profile image"
                  {...fileRef}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        /> */}

        <Label htmlFor="picture">Picture</Label>

        <Input
          id="picture"
          type="file"
          placeholder="profile image"
          {...form.register("picture", { required: true })}
        />
        {form.formState.errors.picture && (
          <div>
            <span>This field is required</span>
          </div>
        )}
        <Button
          disabled={form.formState.isSubmitting}
          type="submit"
          className={"block mx-auto px-8"}
        >
          Submit
        </Button>
      </form>
    </Form>
  );
}

a funkce createProfile volaná v onSubmit: createProfile.ts:

"use server";

import { env } from "@/lib/zod_schemas/envSchema";

type ErrorResponse = {
  errors: ProfileErrorApiResponse;
};

export async function createProfile(
  formData: FormData,
  token: string,
): Promise<ProfileSuccessApiResponse | ErrorResponse> {
  let response: Response;
  try {
    response = await fetch(`${env.API_BASE_URL}/products/profile/`, {
      method: "POST",
      body: formData,
      headers: {
        // "Content-Type": "multipart/form-data",
        Authorization: `JWT ${token}`,
      },
      cache: "no-cache", // This option disables caching
    });
  } catch (error) {
    console.error("Error: ", error);
    throw new Error("Network error");
  }

  if (response.ok) {
    const responseData: ProfileSuccessApiResponse = await response.json();
    return responseData;
  } else if (response.status === 400) {
    const responseError: ProfileErrorApiResponse = await response.json();
    return { errors: responseError };
  } else {
    throw new Error("Unexpected error. Please try again later!");
  }
}

Když odesílám, dostávám tyto chyby z backendu Django:

{
   "errors":{
      "picture":[
         "The submitted data was not a file. Check the encoding type on the form."
      ],
      "user":[
         "Invalid pk \"0\" - object does not exist."
      ]
   }
}

Děkuji vám za vaši pomoc.

Odpověď č. 1

Změňte svou zod schéma na toto, aby bylo akceptovatelné na backendu:

const formSchema = z.object({
  file: typeof window === 'undefined' ? z.any() : z
    .instanceof(FileList)
    .refine((file) => file?.length == 1, 'File is required.')
});

a

const fileRef = form.register("file");
…
<Input type="file" placeholder="shadcn" {...fileRef} />

pokud akce serveru způsobí chybu, zkuste toto :

data={JSON.parse(JSON.stringify(Values))}

NEBO jinak řečeno nekontrolovaný vstup je toto:

<FormField
  control={form.control}
  name="picture"
  render={({ field: { value, onChange, ...fieldProps } }) => (
    <FormItem>
      <FormLabel>Picture</FormLabel>
      <FormControl>
        <Input
          {...fieldProps}
          placeholder="Picture"
          type="file"
          accept="image/*, application/pdf"
          onChange={(event) =>
            onChange(event.target.files && event.target.files[0])
          }
        />
      </FormControl>
      <FormMessage />
    </FormItem>
  )}
/>