Last active
November 13, 2024 12:24
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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