I am trying to use react-hook-form with react context, but the form is not behaving as intended. problems what I am facing
- on submission error states are not triggered.
- using watch() and monitoring changes, I see that it inputs are not controlled and I can't see the changes.
- values like isDirty, isSubmitSuccessful doesn't change from the initial state.
I then moved useForm out of context and directly consumed it inside the component where the form is created, and everything works just fine. I wanted to know if anyone is aware of why is this the case? Can't we use react-hook-form in side a context provider?
below is my attached code for context.
import { zodResolver } from '@hookform/resolvers/zod';
import { createContext, ReactNode, useContext } from 'react';
import { useForm, UseFormReturn } from 'react-hook-form';
import { z } from 'zod';
import { CompanyTypeEnum } from '@/types/buyer.type';
import { CountrySchema, PhoneSchema } from '@/types/common.type';
type BuyerFormContext = UseFormReturn<BuyerForm>;
const BuyerFormContext = createContext<BuyerFormContext | undefined>(undefined);
const BuyerFormSchema = z.object({
id: z.string().uuid().optional(),
companyName: z.string().min(1),
companyType: CompanyTypeEnum,
companyPhone: PhoneSchema,
streetLine1: z.string().min(1),
streetLine2: z.string().optional(),
city: z.string().min(1),
stateOrRegion: z.string().min(1),
postalCode: z.string().min(1),
country: CountrySchema,
website: z.string().min(1),
defaultCurrency: z.string(),
contactName: z.string().min(1),
contactPosition: z.string().min(1),
contactPhone: PhoneSchema,
contactEmail: z.string().email(),
});
export type BuyerForm = z.infer<typeof BuyerFormSchema>;
export const BuyerFormProvider = ({
children,
defaultValues,
}: {
children: ReactNode;
defaultValues?: BuyerForm;
}) => {
const form = useForm<BuyerForm>({
resolver: zodResolver(BuyerFormSchema),
mode: 'onSubmit',
defaultValues: formDefault,
});
return <BuyerFormContext.Provider value={form}>{children}</BuyerFormContext.Provider>;
};
export const useBuyerForm = () => {
const context = useContext(BuyerFormContext);
if (context === undefined) {
throw new Error('useBuyerForm must be used within a BuyerFormProvider');
}
return context;
};
my usage is as below
import { Loader2 } from 'lucide-react';
import { SubmitHandler } from 'react-hook-form';
import { useBlocker } from 'react-router-dom';
import { CustomSelect as CompanyTypeSelect } from '@/components/custom-select';
import { CustomSelect as ContactPositionSelect } from '@/components/custom-select';
import { Button } from '@/components/ui/button';
import { CountryDropdown } from '@/components/ui/country-dropdown';
import { CurrencySelect } from '@/components/ui/currency-select';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { PhoneInput } from '@/components/ui/phone-input';
import { Separator } from '@/components/ui/separator';
import { type BuyerForm, useBuyerForm } from '@/context/buyer-form-context';
import { CURRENCIES } from '@/service/types';
import { COMPANY_TYPES } from '@/types/buyer.type';
import { CONTACT_POSITIONS, Country } from '@/types/common.type';
const BuyerForm = (props: {
onCancel: () => void;
onSubmit: SubmitHandler<BuyerForm>;
formTitle?: string;
submitBtnText?: string;
isFormLoading?: boolean;
isEdit?: boolean;
}) => {
const {
onCancel,
onSubmit,
formTitle = 'Edit Buyer',
submitBtnText = 'Save Changes',
isFormLoading = false,
isEdit = false,
} = props;
const form = useBuyerForm(); // Assuming you are using useForm from react-hook-form
const {
formState: { isDirty, isSubmitSuccessful },
} = form;
useBlocker(({ currentLocation, nextLocation }) => {
if (!isSubmitSuccessful && isDirty && currentLocation.pathname !== nextLocation.pathname) {
return !window.confirm('You have unsaved changes. Are you sure you want to leave?');
}
return false;
});
const onCountryChange = (country: Country) => {
const formValues = form.getValues();
if (!isEdit) {
if (!form.getFieldState('defaultCurrency').isTouched || formValues.defaultCurrency === '') {
const countryCurrency = country.currencies[0];
if ((CURRENCIES as string[]).includes(countryCurrency)) {
form.setValue('defaultCurrency', countryCurrency);
}
}
if (!form.getFieldState('companyPhone').isTouched || formValuespanyPhone.phone === '') {
form.setValue('companyPhone', {
phone: country.countryCallingCodes[0],
country: country,
});
}
}
};
return (
<div className='mt-10'>
<h1 className='h1 lg:mx-auto lg:max-w-screen-md'>{formTitle}</h1>
<Form {...form}>
<form
className='mt-5 pb-20 lg:mx-auto lg:max-w-screen-md'
onSubmit={form.handleSubmit(onSubmit)}>
<div className='flex flex-col justify-between gap-y-2'>
<div className='flex justify-between gap-x-4'>
<FormField
control={form.control}
name='companyName'
render={({ field }) => (
<FormItem className='flex-1'>
<FormLabel>Company Name</FormLabel>
<FormControl>
<Input placeholder='Acme Inc.' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='companyType'
render={({ field }) => (
<FormItem className='flex-1'>
<FormLabel>Company Type</FormLabel>
<FormControl>
<CompanyTypeSelect
onValueChange={field.onChange}
value={field.value}
name={field.name}
ref={field.ref}
options={COMPANY_TYPES}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='flex gap-x-4'>
<FormField
control={form.control}
name='companyPhone'
render={({ field }) => (
<FormItem className='flex-1'>
<FormLabel>Company Phone</FormLabel>
<FormControl>
<FormControl>
<PhoneInput
{...field}
onChange={e => {
field.onChange({
phone: e.target.value.phone,
country: e.target.value.country,
});
}}
value={field.value.phone}
placeholder='Enter your number'
defaultCountry={field.value.country?.alpha2}
className='h-10'
/>
</FormControl>
</FormControl>
<FormDescription>Include country code (e.g. +44)</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name='website'
render={({ field }) => (
<FormItem className='flex-1'>
<FormLabel>Website</FormLabel>
<FormControl>
<Input placeholder='' {...field} className='flex-1' />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<Separator className='my-4 h-px bg-gray-200' />
<div className='space-y-2'>
<h3 className='paragraph-l-bold text-gray-700'>Address</h3>
<div className='space-y-2'>
<div className='flex justify-between gap-x-4'>
<FormField
control={form.control}
name='streetLine1'
render={({ field }) => (
<FormItem className='flex-1'>
<FormLabel>Street Line 1</FormLabel>
<FormControl>
<Input placeholder='e.g., 123 Main Street, Building A' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='streetLine2'
render={({ field }) => (
<FormItem className='flex-1'>
<FormLabel>
Street Line 2 <span className='paragraph-sm text-gray-400'>(optional)</span>
</FormLabel>
<FormControl>
<Input placeholder='e.g., Suite 100, Floor 3' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='flex justify-between gap-x-4'>
<FormField
control={form.control}
name='postalCode'
render={({ field }) => (
<FormItem className='flex-1'>
<FormLabel>Postal Code</FormLabel>
<FormControl>
<Input placeholder='e.g., 94105' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='city'
render={({ field }) => (
<FormItem className='flex-1'>
<FormLabel>City</FormLabel>
<FormControl>
<Input placeholder='e.g., San Francisco' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='flex justify-between gap-x-4'>
<FormField
control={form.control}
name='stateOrRegion'
render={({ field }) => (
<FormItem className='w-[calc(50%-8px)]'>
<FormLabel>State/Region</FormLabel>
<FormControl>
<Input placeholder='e.g., California' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='country'
render={({ field }) => (
<FormItem className='w-[calc(50%-8px)]'>
<FormLabel>Country</FormLabel>
<FormControl>
<CountryDropdown
placeholder='Select a country'
defaultValue={field.value.alpha3}
onChange={country => {
field.onChange(country);
onCountryChange(country);
}}
ref={field.ref}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
<Separator className='my-4 h-px bg-gray-200' />
<div className='space-y-2'>
<h3 className='paragraph-l-bold text-gray-700'>Contact Person</h3>
<div className='flex justify-between gap-x-4'>
<FormField
control={form.control}
name={`contactName`}
render={({ field }) => (
<FormItem className='flex-1'>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder='e.g., John Smith' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`contactPosition`}
render={({ field }) => (
<FormItem className='flex-1'>
<FormLabel>Position</FormLabel>
<FormControl>
<ContactPositionSelect
onValueChange={field.onChange}
value={field.value}
name={field.name}
ref={field.ref}
options={CONTACT_POSITIONS}
placeholder='Select a position'
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='flex justify-between gap-x-4'>
<FormField
control={form.control}
name={`contactEmail`}
render={({ field }) => (
<FormItem className='flex-1'>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder='e.g., [email protected]' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`contactPhone`}
render={({ field }) => (
<FormItem className='flex-1'>
<FormLabel>Phone</FormLabel>
<FormControl>
<div className='flex w-full items-center'>
<CountryDropdown
onChange={country => {
const countryCode = country.countryCallingCodes[0];
const formattedCode = countryCode.startsWith('+')
? countryCode
: `+${countryCode}`;
form.setValue(`contactPhone`, {
phone: formattedCode,
country: country,
});
}}
defaultValue={field.value.country?.alpha3}
inline
/>
<PhoneInput
{...field}
onChange={e => {
field.onChange({
phone: e.target.value.phone,
country: e.target.value.country,
});
}}
value={field.value.phone}
placeholder='Enter your number'
defaultCountry={field.value.country?.alpha2}
inline
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<Separator className='my-4 h-px bg-gray-200' />
<div className='space-y-2'>
<FormField
control={form.control}
name={`defaultCurrency`}
render={({ field }) => (
<FormItem className='w-[calc(50%-8px)]'>
<FormLabel>Payment Currency</FormLabel>
<FormControl>
<CurrencySelect
onValueChange={field.onChange}
placeholder='Select a currency'
disabled={false}
defaultValue={field.value}
currencies='all'
variant='default'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='fixed inset-x-0 bottom-0 border-t border-gray-200 bg-white/70 backdrop-blur-sm'>
<div className='flex justify-end gap-x-4 py-4 lg:mx-auto lg:max-w-screen-md'>
<Button variant='outline' onClick={onCancel} disabled={isFormLoading}>
Cancel
</Button>
<Button type='submit' variant='flume' disabled={isFormLoading}>
{isFormLoading && <Loader2 className='mr-2 size-4 animate-spin' />}
<span>{submitBtnText}</span>
</Button>
</div>
</div>
</form>
</Form>
</div>
);
};
export default BuyerForm;