Search code examples
reactjstypescriptreact-hook-formzodzustand

react-hook-form not working when onsubmit


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.


Solution

  • 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.