Search code examples
next.jsreact-hook-formzodnext.js14shadcnui

Nextjs 14 server side actions with Shadcn form


I want to use Shadcn forms in my NextJS 14 app and want to have form validation like it has currently but when the form is submitted, I want to do a server action. I can either get the validation to work or get the server action to work but both of them together, it doesn't work because the server action requires form data

actions.ts

"use server";

import { formSchema } from "./schema";

export async function onSubmitStudent(
  prevState: { message: string },
  formData: FormData,
) {
  const parse = formSchema.safeParse({
    firstName: formData.get("firstName"),
    lastName: formData.get("lastName"),
  });

  if (!parse.success) {
    // console.log(parse.error);
    // return { message: parse.error };
    const fieldErrors = parse.error.issues.map((issue) => ({
      field: issue.path[0],
      message: issue.message,
    }));

    return { errors: fieldErrors };
  }

  const data = parse.data;
  console.log("Data: ", data);
}

This is where I will make my POST request later on but now I was trying to do form validation here which def didn't work and it was hard to return errors and show them in the UI

page.tsx

const initialState = {
  message: "",
  errors: {},
};

export default function Page() {
  const { pending } = useFormStatus();

  const initialValues: StudentFormValues = {
    firstName: "",
    lastName: "",
  };

  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: initialValues,
  });

  const [state, formAction] = useFormState(onSubmitStudent, initialState);

  useEffect(() => {
    console.log(state?.errors);
    if (Array.isArray(state?.errors)) {
      // Check if state.errors is an array before iterating
      state.errors.forEach((error) => {
        form.setError(error.field, { message: error.message });
      });
    }
  }, [state?.errors]);

  return (
    <Form {...form}>
      <form action={formAction} className="space-y-8">
        <FormField
          control={form.control}
          name="firstName"
          render={({ field }) => (
            <FormItem>
              <FormLabel>First Name</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
              <FormMessage>{state?.errors[0]?.message}</FormMessage>
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="lastName"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Last Name</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
              <FormMessage>{state?.errors[1]?.message}</FormMessage>
            </FormItem>
          )}
        />
        <Button type="submit" disabled={pending}>
          Submit
        </Button>
      </form>
    </Form>
  );
}

So since I have the action={formAction} here, I can't get client side validation to work and getting the server side is pretty hard. How can I either get this where validation is client side but form action is server side?

  1. If I use the client onSubmit function given in Shadcn docs and then call the server action from there, would it still be considered a server call or would it be the same as calling an endpoint?

I have tried adding validation in the actions.ts file and then returning that but displaying that has been a pain.


Solution

  • Nextjs server action with zod validation

    "use server";
    
    import { formSchema } from "./schema";
    
    export async function onSubmitStudent(
      prevState: { message: string },
      formData: FormData,
    ) {
      const parse = formSchema.safeParse({
        firstName: formData.get("firstName"),
        lastName: formData.get("lastName"),
      });
    
      if (!parse.success) {
        return {
          errors: parse.error.flatten().fieldErrors,
          message: undefined
        }
      }
    
      const data = parse.data;
      console.log("Data: ", data);
    

    sent error like this parse.error.flatten().fieldErrors

    then use in component

    export default function Page() {
      const { pending } = useFormStatus();
    
      const initialState = {
        errors: {
          firstName: undefined,
          lastName: undefined
        },
        message: undefined
      };
    
    
      const initialValues: StudentFormValues = {
        firstName: "",
        lastName: "",
      };
    
      const form = useForm<z.infer<typeof formSchema>>({
        resolver: zodResolver(formSchema),
        defaultValues: initialValues,
      });
    
      const [state, formAction] = useFormState(onSubmitStudent, initialState);
    
      useEffect(() => {
        console.log(state?.errors);
        if (Array.isArray(state?.errors)) {
          // Check if state.errors is an array before iterating
          state.errors.forEach((error) => {
            form.setError(error.field, { message: error.message });
          });
        }
      }, [state?.errors]);
    
      return (
        <Form {...form}>
          <form action={formAction} className="space-y-8">
            <FormField
              control={form.control}
              name="firstName"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>First Name</FormLabel>
                  <FormControl>
                    <Input {...field} />
                  </FormControl>
                  <FormMessage>{state?.errors?.firstName}</FormMessage>
                </FormItem>
              )}
            />
            <FormField
              control={form.control}
              name="lastName"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Last Name</FormLabel>
                  <FormControl>
                    <Input {...field} />
                  </FormControl>
                  <FormMessage>{state?.errors?.lastName}</FormMessage>
                </FormItem>
              )}
            />
            <Button type="submit" disabled={pending}>
              Submit
            </Button>
          </form>
        </Form>
      );
    }
    

    here se initialState like this so it have all type safe variables for errors object

    const initialState = {
        errors: {
          firstName: undefined,
          lastName: undefined
        },
        message: undefined
      };
    

    and sometime you may need to show some message error like user not found or somthing in that case you need to retrun from server action

    return {
      errors: undefined,
      message: 'User not found.'
    }
    

    this why you can handle zod error and custom error message as well