In React, using TypeScript, how do I pass a ref to a custom component using RefForwardingComponent and forwardRef?

16,746

The problem was a bad type definition for RefComp. Removing that (letting TypeScript infer the type instead) and the code works just fine.

import * as React from 'react';
import {useRef, useState, forwardRef} from 'react';

interface Props {
    value: string;
    onChange(event: string): void;
}

const RefComp = forwardRef<HTMLInputElement, Props>(
    ({value, onChange}, ref) => (
        <input
            value={value}
            onChange={event => onChange(event.target.value)}
            ref={ref}
        />
    ),
);

export default function App() {
    const [val, setVal] = useState('');
    const inputRef = useRef<HTMLInputElement>(null);

    React.useEffect(() => {
        if (inputRef.current) {
            inputRef.current.focus();
        }
    }, []);

    return (
        <div className="App">
            <RefComp value={val} onChange={setVal} ref={inputRef} />
            <p>{val}</p>
        </div>
    );
}

https://codesandbox.io/s/quizzical-liskov-bsruh

Share:
16,746

Related videos on Youtube

Tobbe
Author by

Tobbe

Motorcycle riding software developer SOreadytohelp

Updated on October 24, 2022

Comments

  • Tobbe
    Tobbe over 1 year

    I'm trying to pass a ref to a custom component (so that I can set focus to the component).

    But I keep getting this error

    const RefComp: React.RefForwardingComponent<HTMLInputElement, Props>
    Type '{ value: string; onChange: Dispatch<SetStateAction<string>>; ref: MutableRefObject<HTMLInputElement | undefined>; }'
    is not assignable to type 'IntrinsicAttributes & Props & { children?: ReactNode; }'.
      Property 'ref' does not exist on type 'IntrinsicAttributes & Props & { children?: ReactNode; }'.ts(2322)
    

    This is my code

    import * as React from 'react';
    import {useRef, useState, RefForwardingComponent, forwardRef} from 'react';
    
    interface Props {
        value: string;
        onChange(event: string): void;
    }
    
    const RefComp: RefForwardingComponent<HTMLInputElement, Props> = forwardRef<
        HTMLInputElement,
        Props
    >(({value, onChange}, ref) => (
        <input
            value={value}
            onChange={event => onChange(event.target.value)}
            ref={ref}
        />
    ));
    
    export default function App() {
        const [val, setVal] = useState('');
        const inputRef = useRef<HTMLInputElement>();
    
        return (
            <div className="App">
                <RefComp value={val} onChange={setVal} ref={inputRef} />
                <p>{val}</p>
            </div>
        );
    }
    

    Here's a codesandbox https://codesandbox.io/s/crimson-cdn-klfp5

    The code seems to work fine, if I ignore the error. So not sure why I'm getting it...

    Can anyone please explain why I get the error, and how I fix it? :)

    EDIT:

    As mentioned in the comments you can work around this by adding ref to your props.

    import * as React from 'react';
    import {
        useRef,
        useState,
        RefForwardingComponent,
        forwardRef,
        MutableRefObject,
    } from 'react';
    
    interface Props {
        value: string;
        onChange(event: string): void;
        ref?: MutableRefObject<HTMLInputElement | null>;
    }
    
    const RefComp: RefForwardingComponent<HTMLInputElement, Props> = forwardRef<
        HTMLInputElement,
        Props
    >(({value, onChange}, ref) => (
        <input
            value={value}
            onChange={event => onChange(event.target.value)}
            ref={ref}
        />
    ));
    
    export default function App() {
        const [val, setVal] = useState('');
        const inputRef = useRef<HTMLInputElement>(null);
    
        return (
            <div className="App">
                <RefComp value={val} onChange={setVal} ref={inputRef} />
                <p>{val}</p>
            </div>
        );
    }
    

    But this feels wrong. Now we're saying we have ref both as parts of the props, and as a second callback argument to the function passed to forwardRef

    My issue with this would probably be clearer if I wrote it like this

    const RefComp: RefForwardingComponent<HTMLInputElement, Props> = forwardRef<
        HTMLInputElement,
        Props
    >((props: Props, ref) => (
        <input
            value={props.value}
            onChange={event => props.onChange(event.target.value)}
            ref={ref /* here, props.ref is also available, but wouldn't work */ }
        />
    ));
    

    EDIT 2:

    Here's a related question

    Typescript RefForwardingComponent not working

    EDIT 3:

    I found the relevant source code. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/33f6179e0f25b0ca798ad89a667c0a27ea0c98dd/types/react/index.d.ts#L702

    function forwardRef<T, P = {}>(Component: RefForwardingComponent<T, P>): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;
    

    So the forwardRef function returns a component of type ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>. This is a component that takes props of type PropsWithoutRef<P> intersected with RefAttributes<T>. Both types are also defined in the same file.

    From what I understand the first one basically just excludes ref from P (which is Props in my original example). The second one is defined as

    interface RefAttributes<T> extends Attributes {
        ref?: Ref<T>;
    }
    

    Given this information I really don't understand why I have to add ref to my own Props. It really looks as if forwardRef should do it for me - even going as far as actually removing ref from my own Props...

    Can someone please explain what's going on here?

    • keikai
      keikai over 4 years
      Directly add ref declaration to interface Props seems work well