search

Home  >  Q&A  >  body text

react-hook-form + chakra-ui + any phone number library (probably duel reference issue)

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:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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:

1

2

3

4

5

6

7

8

9

10

11

12

13

<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

1

2

3

4

5

6

<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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

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粉143640496P粉143640496395 days ago688

reply all(1)I'll reply

  • P粉680487967

    P粉6804879672024-03-28 00:17:15

    The tip from @adsy in the comments solved this problem.

    Use useController in components:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    37

    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>

                            }

                        />

                    </countryselector></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 minor changes to ref in components).

    Then when you call it for deep nesting, also destructure control:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

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

                    </phonenumberinput></formcontrol></form></connectform>

    reply
    0
  • Cancelreply