Using a forwardRef component with children in TypeScript
Asked Answered
D

5

125

Using @types/react 16.8.2 and TypeScript 3.3.1.

I lifted this forward refs example straight from the React documentation and added a couple type parameters:

const FancyButton = React.forwardRef<HTMLButtonElement>((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<HTMLButtonElement>();
<FancyButton ref={ref}>Click me!</FancyButton>;

I get the following error in the last line under FancyButton:

Type '{ children: string; ref: RefObject<HTMLButtonElement>; }' is not assignable to type 'IntrinsicAttributes & RefAttributes<HTMLButtonElement>'. Property 'children' does not exist on type 'IntrinsicAttributes & RefAttributes<HTMLButtonElement>'.ts(2322)

It would seem that the type definition for React.forwardRef's return value is wrong, not merging in the children prop properly. If I make <FancyButton> self-closing, the error goes away. The lack of search results for this error leads me to believe I'm missing something obvious.

Davisson answered 12/2, 2019 at 16:12 Comment(0)
O
177

You need to pass the button properties:

import * as React from 'react'

type ButtonProps = React.HTMLProps<HTMLButtonElement>

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

// You can now get a ref directly to the DOM button:
const ref = React.createRef<HTMLButtonElement>()

<FancyButton ref={ref}>Click me!</FancyButton>

ADDED:

In recent versions of TS and @types/react, you can also use React.ComponentPropsWithoutRef<'button'> instead of React.HTMLProps<HTMLButtonElement>

Ob answered 13/2, 2019 at 19:7 Comment(3)
Wow thank you, React.ComponentPropsWithoutRef works perfectly. How did you discover that existed? In @types/react, IntrinsicElements.button's type is defined as React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>. I was pulling my hair out trying to work with that typeForetooth
Weston, it was by logic, trying to find a definition to replace "ref" in typings of react-bootstrap.Ob
I hate TS for things like thisZarf
V
55

The answers given by aMarCruz and euvs both work, but they lie to consumers a little bit. They say they accept all HTMLButtonElement props, but they ignore them instead of forwarding them to the button. If you're just trying to merge in the children prop correctly, then you might want to use React.PropsWithChildren instead:

import React from 'react';

interface FancyButtonProps {
    fooBar?: string; // my custom prop
}

const FancyButton = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<FancyButtonProps>>((props, ref) => (
    <button type="button" ref={ref} className="fancy-button">
        {props.children}
        {props.fooBar}
    </button>
));

FancyButton.displayName = 'FancyButton';

Or explicitly add a children prop:

interface FancyButtonProps {
    children?: React.ReactNode;
    fooBar?: string; // my custom prop
}

const FancyButton = React.forwardRef<HTMLButtonElement, FancyButtonProps>((props, ref) => (
    <button type="button" ref={ref} className="fancy-button">
        {props.children}
        {props.fooBar}
    </button>
));

FancyButton.displayName = 'FancyButton';

Or if you actually want to accept all the button props and forward them (let consumers choose button type="submit", for example), then you might want to use rest/spread:

import React from 'react';

interface FancyButtonProps extends React.ComponentPropsWithoutRef<'button'> {
    fooBar?: string; // my custom prop
}

const FancyButton = React.forwardRef<HTMLButtonElement, FancyButtonProps>(
    ({ children, className = '', fooBar, ...buttonProps }, ref) => (
        <button {...buttonProps} className={`fancy-button ${className}`} ref={ref}>
            {children}
            {fooBar}
        </button>
    ),
);

FancyButton.displayName = 'FancyButton';
Villareal answered 30/3, 2021 at 18:47 Comment(2)
...buttonProps }: FancyButtonProps, ref)Interlace
This should be the correct answer. PropsWithChildren is exactly what was needed.Dvina
P
15

The answer given by aMarCruz works well. However, if you also need to pass custom props to the FancyButton, here is how it can be done.

interface FancyButtonProps extends React.ComponentPropsWithoutRef<'button'> {
    fooBar?: string; // my custom prop
}

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


/// Use later

// You can now get a ref directly to the DOM button:
const ref = React.createRef<HTMLButtonElement>()

<FancyButton ref={ref} fooBar="someValue">Click me!</FancyButton>

Just adding here for completion.

Paradox answered 16/12, 2020 at 23:54 Comment(0)
U
5

You can use ForwardRefRenderFunction<YourRefType, YourProps> on your component.

Like:

const Component: ForwardRefRenderFunction<YourRef, YourProps> = (yourProps, yourRef) => return <></>
export default fowardRef(Component)
Unbacked answered 6/1, 2023 at 15:20 Comment(0)
M
0

I had the same issue with javascript where I was not able to get the types of props for the autocompletion,

I used the following trick to make the completion work.

const _false = false;
export const MyComponent =
  (_false ? ({ label }) => null : _false) || forwardRef((data, ref) => <input />);
Manicurist answered 9/8, 2023 at 15:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.