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?
I have tried adding validation in the actions.ts file and then returning that but displaying that has been a pain.
"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