I'm using react-hook-form to build generic form components that are deeply nested and referenced via the useFormContext paradigm to enable arbitrarily deep nesting of components. I used Chakra-UI for styling. This all works fine. However, I would like to add an international phone number input to some forms.
I don't really care which library I use as long as it's performant in a NextJS context and works with RHF and Chakra, so I'm open to suggestions in a completely different direction than below.
I think I'm very close to using react-international-phone.
The problem I'm having (I've had similar but slightly different problems with other libraries) is that react-international-phone works well with either Chakra or react-hook-form, but not both at the same time Use both at the same time.
In the Github source code, react-international-phone has a Storybook example integrated with Chakra-UI, which works as follows:
export const ChakraPhone = ({ value, onChange, }) => { const phoneInput = usePhoneInput({ defaultCountry: 'us', value, onChange: (data) => { onChange(data.phone); }, }); return ( <ChakraProvider> <Input value={phoneInput.phone} onChange={phoneInput.handlePhoneValueChange} ref={phoneInput.inputRef} /> </ChakraProvider> ); };
If I just use the Chakra Input
component in react-hook-forms, it would look like this:
<ConnectForm> {({ formState: { errors }, register}) => ( <form> <FormControl isInvalid={errors.phone}> <FormLabel htmlFor='phone'>Phone Number</FormLabel> <Input id='phone' name='phone' {...register('phone')} /> </FormControl> </form> )} </ConnectForm>
The two issues with combining these two things are that ...register
returns ref
to the html input, and React-international-phone needs to onChange
Passed as a property to its usePhoneInput
hook.
For the first question I thought I could use this answer and do
<Input value={phoneInput.phone} onChange={phoneInput.handlePhoneValueChange} name={name} ref={(el) => {reactHookRef(el); phoneInput.inputRef(el)}} />
But complains phoneInput.inputRef
is an object not a function. In fact, the docs say it's a React.RefObject<HTMLInputElement>
, which... I guess isn't a function. But then I'm not sure why ref={phoneInput.inputRef}
works in the example code.
I think I can solve the second problem by refactoring the react-hook-form register
response and passing the returned onChange
to the usePhoneInput
hook .
Initially I tried this
const PhoneNumberInput = (props) => { return ( <ConnectForm> {({ formState: { errors }, register }) => { const { onChange, onBlur, name, ref: reactHookRef } = register('phone'); const phoneInput = usePhoneInput({ defaultCountry: 'gb', onChange: onChange }) return ( <ConnectForm> <Input type='tel' value={phoneInput.phone} onChange={phoneInput.handlePhoneValueChange} name={name} ref={(el) => {reactHookRef(el); phoneInput.inputRef}} />
But the problem is usePhoneInput
is a hook, so it can't actually be called there. My current location is
const PhoneNumberInput = (props) => { const [ onChangeRHF, setOnChangeRHF ] = useState(); const phoneInput = usePhoneInput({ defaultCountry: 'gb', onChange: onChangeRHF }) return ( <ConnectForm> {({ formState: { errors }, register }) => { const { onChange, onBlur, name, ref: reactHookRef } = register('phone'); setOnChangeRHF(onChange); return ( <> <InputGroup size={props?.size} id={props?.id || 'phone'}> <InputLeftAddon width='4rem'> <CountrySelector selectedCountry={phoneInput.country} onSelect={(country) => phoneInput.setCountry(country.iso2)} renderButtonWrapper={({ children, rootProps }) => <Button {...rootProps} variant={'outline'} px={'4px'} mr={'8px'}> {children} </Button> } /> </InputLeftAddon> <Input type='tel' value={phoneInput.phone} onChange={phoneInput.handlePhoneValueChange} onBlur={onBlur} name={name} ref={(el) => {reactHookRef(el); phoneInput.inputRef}} />
Ifeel is close, but it still doesn't work. I've put it into CodeSandbox. CodeSandbox is broken, in App.js I commented out the call to the form because if I uncomment it it locks up my browser :(
Any ideas on how to connect react-hook-form and chakra-ui with this or any other phone number library?
P粉6804879672024-03-28 00:17:15
The tip from @adsy in the comments solved this problem.
Use useController
in components:
const PhoneNumberInput = ({ control, name, size='md' }) => { const { field, fieldState: { invalid, isTouched, isDirty }, formState: { touchedFields, dirtyFields } } = useController({ name, control }) const phoneInput = usePhoneInput({ defaultCountry: 'gb', onChange: (data) => { field.onChange(data.phone); } }) return ( <>> ) }; export default PhoneNumberInput; field.ref(el) && phoneInput.inputRef} /> phoneInput.setCountry(country.iso2)} renderButtonWrapper={({ children, rootProps }) => } />
(and minor changes to ref
in components).
Then when you call it for deep nesting, also destructure control
:
{({ control, formState: { errors }, register}) => (