Server action function not being called Nextjs Shadcn React hook form

87 Views Asked by At

This is the form i made using Shadcn just like it says in the docs:

/app/admin/products/_components/ProductForm.tsx

"use client";

import { addProduct } from "@/app/admin/_actions/products";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { NewProductSchema } from "@/zod/schemas";

import { formatCurrency } from "@/lib/formatters";

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

export function ProductForm() {
  const form = useForm<z.infer<typeof NewProductSchema>>({
    resolver: zodResolver(NewProductSchema),
    defaultValues: {
      name: "",
      description: "",
      priceInCents: 200,
      file: undefined,
      image: undefined,
    },
  });

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

  const [priceInCents, setPriceInCents] = useState<number>(200);

  async function onSubmit(values: z.infer<typeof NewProductSchema>) {
    console.log(values);
    await addProduct(values);
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Name</FormLabel>
              <FormControl>
                <Input placeholder="name" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        //.... more fields

        <Button disabled={form.formState.isSubmitting} type="submit">
          {form.formState.isSubmitting ? "Saving..." : "Save"}
        </Button>
      </form>
    </Form>
  );
}

I tried to trim it down as much as possible. As you can see, this compoentn is a client component becasue of the "use client" at the top. I wrote a separate function that i want to be run on the server since it requires the prisma client and the fs which can only be run on the server:

/app/admin/_actions/products:

"use server";

import fs from "fs/promises";
import { redirect } from "next/navigation";
import { z } from "zod";

import { NewProductSchema } from "@/zod/schemas";

import { prisma } from "@/lib/prismaClient";

export const addProduct = async (values: z.infer<typeof NewProductSchema>) => {
  const result = NewProductSchema.safeParse(values);
  console.log(result);
  if (result.success === false) {
    return result.error.formErrors.fieldErrors;
  }

  const data = result.data;

  fs.mkdir("products", { recursive: true });
  const filePath = `products/${crypto.randomUUID()}-${data.file.name}`;
  await fs.writeFile(filePath, Buffer.from(await data.file.arrayBuffer()));

  fs.mkdir("public/products", { recursive: true });
  const imagePath = `/products/${crypto.randomUUID()}-${data.image.name}`;
  await fs.writeFile(
    `public${imagePath}`,
    Buffer.from(await data.image.arrayBuffer()),
  );

  await prisma.product.create({
    data: {
      name: data.name,
      description: data.description,
      priceInCents: data.priceInCents,
      filePath,
      imagePath,
    },
  });

  redirect("/admin/products");
};

If i remove "use server" from the top, i get errors like fs cannot imported etc meaning that the function cannot be run on the client as i said. My problem is that when i click on the submit button on the form, the onSubmit function does run, it logs the values, but it never runs the addProduct function. Does anyone know why this is happening? I admit i maybe am not that good at programming and i probably wrote something stupid but nextjs is pissing me off.

Edit: Here is the zod schema if it makes a difference:

export const NewProductSchema = z.object({
  name: z.string().min(2).max(50).trim(),
  priceInCents: z.coerce
    .number()
    .min(200, { message: "The minimum price for a product is 2 dolars" })
    .positive({ message: "The price must be a positive number" }),
  description: z.string().min(8, {
    message: "The description must be at least 8 characters long",
  }),

  file: z
    .any()
    .refine((file) => file?.length == 1, "File is required.")
    .refine(
      (file) => file[0]?.type.startsWith("video/"),
      "Must be a png, jpeg or jpg.",
    )
    .refine((file) => file[0]?.size <= 5000000, `Max file size is 5MB.`),

  image: 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.`),
});

Here is the link to the github repo if you want to recreate it. go to the admin/products/new route.

1

There are 1 best solutions below

0
Hashan Hemachandra On

The issue is with the onSubmit function itself.

In your onSubmit function, you're logging the values and then calling await addProduct(values). However, the addProduct function is a server-side function, and you're trying to call it directly from the client-side component.

To fix this, you need to move the call to addProduct to a separate server-side action, which can be done using the Next.js action function.

Create a new file inside the app/admin/products directory, let's call it addProduct.tsx,

import { NextResponse } from "next/server";
import { z } from "zod";
import { NewProductSchema } from "@/zod/schemas";
import { addProduct } from "@/app/admin/_actions/products";

export async function POST(request: Request) {
  const formData = await request.formData();
  const result = NewProductSchema.safeParse(Object.fromEntries(formData));

  if (result.success === false) {
    return NextResponse.json(result.error.formErrors.fieldErrors, {
      status: 400,
    });
  }

  const data = result.data;
  await addProduct(data);

  return NextResponse.redirect("/admin/products");
}

Above code creates a new server-side action that can be triggered by a POST request. It validates the form data using the NewProductSchema, and if the data is valid, it calls the addProduct function from the server-side action file.

Then, inside your ProductForm component, update the onSubmit function to use the new server-side action:

async function onSubmit(values: z.infer<typeof NewProductSchema>) {
  const formData = new FormData();
  Object.entries(values).forEach(([key, value]) => {
    formData.append(key, value);
  });

  await fetch("/admin/products/addProduct", {
    method: "POST",
    body: formData,
  });
}