philolegein
philolegein

Reputation: 1535

react-hook-form + chakra-ui + any phone number library (maybe duelling ref problem)

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

Answers (1)

philolegein
philolegein

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

Related Questions