Skip to content

Instantly share code, notes, and snippets.

@humayunkabir
Last active November 13, 2024 12:24
Show Gist options
  • Save humayunkabir/771455e22d3e21940297bf7220851f42 to your computer and use it in GitHub Desktop.
Save humayunkabir/771455e22d3e21940297bf7220851f42 to your computer and use it in GitHub Desktop.
[NextJS] Server + Client validation on Form submit using react-hook-form, zod, and server action
'use server';
import { signInSchema } from '@/schema';
import { ZodType } from 'zod';
export async function action<T>(
formData: FormData,
schema: ZodType<T>
): Promise<TFormState<T>> {
const data = Object.fromEntries(formData.entries());
const result = schema.safeParse(data);
if (!result.success) {
return {
ok: false,
errors: result.error.flatten().fieldErrors as TFieldErrors<T>,
};
}
return {
ok: true,
data: result.data,
};
}
export async function signInAction(formData: FormData) {
return action(formData, signInSchema);
}
type TFieldErrors<T> = Record<keyof T, string[]>;
type TFormStateError<T> = {
ok: false;
errors: TFieldErrors<T>;
data?: never;
};
type TFormStateSuccess<T> = {
ok: true;
data: T;
errors?: never;
};
type TFormState<T> = TFormStateError<T> | TFormStateSuccess<T>;
'use client';
import { signInAction } from '@/actions';
import { setErrors, validate } from '@/actions/validate';
import { signInSchema, TSignInField } from '@/schema';
import { zodResolver } from '@hookform/resolvers/zod';
import { signIn } from 'next-auth/react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
export default function SignIn() {
const {
register,
handleSubmit,
setError,
formState: { errors },
} = useForm<TSignInField>({
mode: 'all',
resolver: zodResolver(signInSchema),
});
const onSubmit: SubmitHandler<TSignInField> = async (data) => {
try {
const validatedData = await validate(
data,
signInAction,
setErrors.bind(setError)
);
await signIn('credentials', validatedData);
} catch (error) {
console.log(error);
}
};
return (
<form className='flex flex-col gap-4' onSubmit={handleSubmit(onSubmit)}>
<div>
<Input type='email' placeholder='Email' {...register('email')} />
{errors?.email && (
<p className='text-red-500'>{errors.email.message}</p>
)}
</div>
<div>
<Input
type='password'
placeholder='Password'
{...register('password')}
/>
{errors?.password && (
<p className='text-red-500'>{errors.password.message}</p>
)}
</div>
<Button size='lg' type='submit'>
Sign in
</Button>
</form>
);
}
import { z } from 'zod';
export const signInSchema = z.object({
email: z.string().email('Enter a valid email.').trim(),
password: z
.string()
.min(6, 'Password must contain at least 6 characters')
.max(30, 'Password must contain at most 30 characters')
.trim(),
});
export type TSignInField = z.infer<typeof signInSchema>;
import { FieldValues, Path, UseFormSetError } from 'react-hook-form';
export async function validate<T extends FieldValues>(
data: T,
action: (formData: FormData) => Promise<TFormState<T>>,
onError?: (errors: TFieldErrors<T>) => void
) {
const formData = new FormData();
Object.entries(data).map(([name, value]) => {
formData.append(name, value);
});
const response = await action(formData);
return new Promise<T>((resolve, reject) => {
if (response.ok) {
resolve(response.data);
}
if (response.errors) {
reject(response.errors.toString());
if (onError) onError(response.errors);
}
});
}
export function setErrors<T extends FieldValues>(errors: TFieldErrors<T>) {
return (setError: UseFormSetError<T>) => {
Object.entries(errors).map(([fieldName, value]) => {
setError(fieldName as Path<T>, { message: value[0] });
});
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment