Reputation: 1535
I'm using react-hook-form to build generic form components, which get deeply nested and referenced through the useFormContext paradigm to enable arbitrary deep nesting of the components. I'm using Chakra-UI for the styling. This is all working fine. However, I'd like to add international phone-number inputs to some forms.
I don't actually 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 that go a completely different direction than below.
I think I've gotten quite close using react-international-phone.
The issue I'm having (and I've run into similar, but slightly different issues with other libraries) is that react-international-phone works well with Chakra, or with react-hook-form, but not with both at the same time.
In the Github source, react-international-phone has a Storybook example of integration with Chakra-UI that works like this:
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 were just using the Chakra Input
component inside react-hook-forms, it would look something 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 problems with combining these two things are that ...register
returns a ref
to the html input, and that react-international-phone needs onChange
passed as a property to its usePhoneInput
hook.
For the first problem, 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 that complains that phoneInput.inputRef
is an Object not a function. The docs say, in fact, that it is a React.RefObject<HTMLInputElement>
, which ... I guess is not a function. But then I'm not sure why ref={phoneInput.inputRef}
works in the sample code.
The second problem I thought I could solve by restructuring the react-hook-form register
response and passing the returned onChange
to the usePhoneInput
hook.
Initially I tried this as
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 there is that usePhoneInput
is a hook, so it can't actually be called there. Where I currently am 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}}
/>
I feel like this is close, but it's still not working. I've put it in a CodeSandbox. The CodeSandbox is broken, and in App.js I've commented out the call to the form, because if I un-comment it, it locks up my browser :(
Any ideas on how I can connect react-hook-form and chakra-ui with this, or any other, phone number library?
Upvotes: 0
Views: 1767
Reputation: 1535
The hint fro @adsy in the comments got it solved.
Use useController
in the component:
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 (
<>
<InputGroup size={size} id={name}>
<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={field.onBlur}
name={name}
ref={(el) => field.ref(el) && phoneInput.inputRef}
/>
</InputGroup>
</>
)
};
export default PhoneNumberInput;
(and the slight change to the ref
in the component).
Then, when you call it for the deep nesting, destructure control
as well:
<ConnectForm>
{({ control, formState: { errors }, register}) => (
<form>
<FormControl isInvalid={errors.phone}>
<FormLabel htmlFor='phone'>Phone Number</FormLabel>
<PhoneNumberInput name='phone' control={control} />
<FormErrorMessage>
{errors.phone && errors.phone.message}
</FormErrorMessage>
</FormControl>
Upvotes: 0