Valerio
Valerio

Reputation: 3617

React nested forwardRef

In my React (w/ typescript) application I've created a form using react-hook-form to manage all the logic of it.

I've then customised the select element with some css and other things. But, for the sake of question's simplicity, here the barebone component:

import { forwardRef } from 'react';

type Props = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>;

const Select = forwardRef<HTMLSelectElement, Props>((props, ref) => {
    return (
        <select ref={ref} {...props}>
            <option value="">...</option>
            {props.children}
        </select>
    );
});

export default Select;

I've then defined another component that "specialise" the previous one, as:

import { forwardRef } from 'react';

import Select from './select';

type Props = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLSelectElement>, HTMLSelectElement> & { label: string; errors?: string };

const SelectWitLargeData = forwardRef<HTMLSelectElement, Props>((props, ref) => {
    return (
        <Select ref={ref} {...props}>
           ...large amount of options
        </Select>
    );
});

export default SelectWitLargeData;

The problem is that the compiler gives me an error on <Select ref={ref} (<- here) ...>

Type 'ForwardedRef<HTMLSelectElement> | LegacyRef<HTMLSelectElement>' is not assignable to type 'Ref<HTMLSelectElement>'.
  Type 'string' is not assignable to type 'Ref<HTMLSelectElement>'.ts(2322)
index.d.ts(140, 9): The expected type comes from property 'ref' which is declared here on 

type 'IntrinsicAttributes & Pick<Props, "label" | "key" | keyof InputHTMLAttributes<HTMLSelectElement> | "errors"> & RefAttributes<...>'

(JSX attribute) RefAttributes<HTMLSelectElement>.ref?: Ref<HTMLSelectElement>

I've search for solutions online, and found many suggestions, but none of these work.

Any help is appreciated Thanks in advance!

Edit Here's an example form to consume the components

import { useEffect } from 'react';
import { useForm } from 'react-hook-form';

import Select from '@components/select';
import SelectWithLargeData from '@components/select-with-large-data';

type Form = {
    email: string;
    state: string;
    country: string;
};

const Address: React.FC = () => {
    const { handleSubmit, register, setValue } = useForm<Form>();

    useEffect(() => {
        setValue('state', 'AL');
    }, [setValue]);

    const onSubmit = async (input: Form) => {
        console.log(input);
    };

    return (
        <>
            <form onSubmit={handleSubmit(onSubmit)} className="space-y-10">
                <input type="email" {...register('email', { required: true })} />

                <SelectWithLargeData label="Provincia" {...register('state', { required: true })} />

                <Select label="Stato" {...register('country', { required: true })}>
                    <option value="IT">Italia</option>
                </Select>
            </form>
        </>
    );
};

export default Address;

Edit 2 https://codesandbox.io/s/withered-rain-w9ej4


Upvotes: 7

Views: 5185

Answers (2)

Nikecow
Nikecow

Reputation: 31

As the edit queue is full, I'll add a separate answer. With this you can actually get the ref from HTML select element all the way up to the form. Is this what you are looking for?

import React, { forwardRef } from 'react';

type Props = Omit<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>, 'ref'>;

const Select = forwardRef<HTMLSelectElement, Props>((props, ref) => (
    <select {...props} ref={ref}>
        <option value="">...</option>
        {props.children}
    </select>
    )
);

type SelectHigherOrder = Props & { label: string; errors?: string };

const SelectWithLargeData = forwardRef<HTMLSelectElement, SelectHigherOrder>((props, ref) => (
  <Select {...props} ref={ref} >
    ...large amount of options
  </Select>
);

Also I would recommend to spread the props first and then add your own props, in order to avoid overwriting.

Upvotes: 3

Please see forwarding-refs docs:

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

You should create ref inside SelectWitLargeData instead of forwarding it again.

Consider this example:

import React, { forwardRef } from 'react';

type Props = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>;

const Select = forwardRef<HTMLSelectElement, Props>((props, ref) => {
  return (
    <select ref={ref} {...props}>
      <option value="">...</option>
      {props.children}
    </select>
  );
});

type SelectHigherOrder = Props & { label: string; errors?: string };

const SelectWitLargeData = (props: Omit<SelectHigherOrder, 'ref'>) => {
  const ref = React.useRef<HTMLSelectElement>(null);

  return (
    <Select ref={ref} {...props}>
      1
    </Select>
  );
}

Playground

You probably might have noticed that I have used Omit<SelectHigherOrder, 'ref'>. This is because you are using this type: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>;. And this type contains legacy ref type: type WithLegacyRef = Props['ref'] and when you use {...props} after ref={ref} you are actually overriding correct ref type which is React.RefObject<HTMLSelectElement>

Upvotes: 2

Related Questions