Upon clicking the SubmitBtn in the LoginForm component for the first time, the form fails to submit, and nothing appears to happen. The onInvalid function returns an empty object ({}). However, on the second attempt, the onInvalid function provides the following feedback:
{
username: { message: 'Required', type: 'invalid_type', ref: undefined },
confirmPassword: { message: 'Required', type: 'invalid_type', ref: undefined }
}
Despite this detailed validation error, the form remains unsubmitted, and no observable changes occur.
LogInForm :
import { FormControl } from '@chakra-ui/react'
import SubmitBtn from '../SubmitBtn'
import InputComponent from '../InputComponent';
import LoginService from '../../../services/loginService';
const LoginForm = () => {
const { handleSubmit, onsubmit, register, isPending, errors, onInvalid } = LoginService()
return (
<form onSubmit={handleSubmit(onsubmit, onInvalid)}>
<FormControl isRequired mb='4'>
<InputComponent label='Email Address' register={register} errorsType={errors} errorsMessage={errors.email?.message} placeholder='[email protected]' type='email' name='email' helper='please make sure to enter a valid email address' />
</FormControl>
<FormControl isRequired mb='4'>
<InputComponent label='Password' register={register} errorsType={errors} errorsMessage={errors.password?.message} placeholder='enter your password: ******' type='password' name='password' helper='please make sure to enter the correct password' />
</FormControl>
<SubmitBtn loading={isPending} textloading='logging in' >Login</SubmitBtn>
</form>
)
}
export default LoginForm
InputComponent
import { FormHelperText, Input, FormLabel } from '@chakra-ui/react'
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
import { FormType } from '../../types/types';
type InputComponentPropsTypes = {
register: UseFormRegister<FormType>,
errorsType?: FieldErrors<FormType> | undefined,
errorsMessage: string | undefined,
type: React.HTMLInputTypeAttribute,
name: keyof FormType
label: string
placeholder: string,
helper: string
}
const InputComponent = ({ label, errorsType, errorsMessage, register, name, type, placeholder, helper }: InputComponentPropsTypes) => {
return (
<>
<FormLabel>{label}</FormLabel>
<Input {...register(name)} type={type} placeholder={placeholder} focusBorderColor='teal.500' />
{errorsType && errorsType[name] ?
<FormHelperText color='red'>{errorsMessage}</FormHelperText>
:
<FormHelperText>{helper}</FormHelperText>
}
</>
)
}
export default InputComponent
LoginService
import { useToast } from '@chakra-ui/react'
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form'
import axios from 'axios'
import { useMutation } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import { z } from 'zod'
import { useStore } from '../utils/store';
import { FormSchema, FormType } from '../types/types';
import { API_URL } from '../constants';
const LoginService = () => {
const setToken = useStore(store => store.setToken)
const toast = useToast()
const navigation = useNavigate()
const { reset, register, handleSubmit, formState: { errors } } = useForm<FormType>({ resolver: zodResolver(FormSchema) })
const { mutateAsync: loginUser, isPending } = useMutation({
mutationKey: ['loginUser'],
mutationFn: async (user: FormType) => {
await axios.post(`${API_URL}auth/login`, user).then(res => {
setToken(res.data.token)
localStorage.setItem('token', res.data.token)
})
reset()
}
})
const onInvalid = () => console.log(errors)
const onsubmit = (user: FormType) => {
console.log('clicked')
loginUser(user, {
onSuccess: () => {
toast({
title: 'successfully logged in',
position: 'top',
status: 'success',
isClosable: true,
})
navigation('/home')
},
onError: (error) => {
if (error instanceof z.ZodError) {
toast({
title: `${error.issues[0].message}`,
position: 'top',
status: 'error',
isClosable: true,
})
} else toast({
title: 'something went wrong',
position: 'top',
status: 'error',
isClosable: true,
})
}
})
}
return {
onsubmit,
onInvalid,
register,
handleSubmit,
isPending,
errors
}
}
export default LoginService
Types
import { z } from "zod"
export type AuthType = 'register' | 'login'
export const FormSchema = z.object({
username: z.string().nullable(),
email: z.string().email(),
password: z.string().min(8, { message: 'Must be 8 or more characters long' }),
confirmPassword: z.string().nullable()
}).refine(data => data.password === data.confirmPassword, {
message: 'confirmed password must match the password',
path: ['confirmPassword']
})
export type FormType = z.infer<typeof FormSchema>;
The form was functioning correctly initially. However, after splitting it into multiple components, the issue arose.
The solution I found is to add a new schema for the login.
export type AuthType = 'register' | 'login'
export const RegisterFormSchema = z.object({
username: z.string().optional(),
email: z.string().email(),
password: z.string().min(8, { message: 'Must be 8 or more characters long' }),
confirmPassword: z.string().optional()
}).refine(data => data.password === data.confirmPassword, {
message: 'confirmed password must match the password',
path: ['confirmPassword']
})
export const LoginFormSchema = z.object({
email: z.string().email(),
password: z.string().min(8, { message: 'Must be 8 or more characters long' }),
})
export type FormType = z.infer<typeof RegisterFormSchema>;
And picking only the email and password from FormType in loginService.
import { useToast } from '@chakra-ui/react'
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form'
import axios from 'axios'
import { useMutation } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import { z } from 'zod'
import { useStore } from '../utils/store';
import { FormType, LoginFormSchema, RegisterFormSchema } from '../types/types';
import { API_URL } from '../constants';
type logInFormType = Pick<FormType, "email" | "password">
export const LoginService = () => {
const setToken = useStore(store => store.setToken)
const toast = useToast()
const navigation = useNavigate()
const { register, handleSubmit, formState: { errors }, reset } = useForm<logInFormType>({ resolver: zodResolver(LoginFormSchema) })
const { mutateAsync: loginUser, isPending } = useMutation({
mutationKey: ['loginUser'],
mutationFn: async (user: logInFormType) => {
await axios.post(`${API_URL}auth/login`, user).then(res => {
setToken(res.data.token)
localStorage.setItem('token', res.data.token)
})
reset()
}
})
const onsubmit = (user: logInFormType) => {
loginUser(user, {
onSuccess: () => {
toast({
title: 'successfully logged in',
position: 'top',
status: 'success',
isClosable: true,
})
navigation('/home')
},
onError: (error) => {
if (error instanceof z.ZodError) {
toast({
title: `${error.issues[0].message}`,
position: 'top',
status: 'error',
isClosable: true,
})
} else toast({
title: 'something went wrong',
position: 'top',
status: 'error',
isClosable: true,
})
}
})
}
return {
onsubmit,
register,
handleSubmit,
isPending,
errors
}
}
It should work with the optional() method, but I don't know why it didn't work.