Using a forwardRef component with children in TypeScript - reactjs

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.

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

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

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.

You can use ForwardRefRenderFunction<YourRefType, YourProps> on your component.
Like:
const Component: ForwardRefRenderFunction<YourRef, YourProps> = (yourProps, yourRef) => return <></>
export default fowardRef(Component)

Related

"Expected 1 arguments, but got 0." for onClick event in React with Typescript

I have a handleClick function that I'm trying to pass as a prop for my onClick event within a component. This event just takes a setState function. I set an interface to type this as "handleClick: React.Dispatch<React.SetStateAction>;" as nothing else worked and would always give errors, so I assumed all was well, until I went ahead with writing the onClick event into the component declaration, when the error in the title appeared.
Here's the relevant code:
interface IProps {
handleClick: React.Dispatch<React.SetStateAction<boolean>>;
icon?: JSX.Element;
}
const NavLinks: React.FC<IProps> = ({ handleClick }) => (
<div className="sidebar_navlinks">
{sidebar_links.map((link) => (
<NavLink key={link.name} to={link.to} onClick={() => handleClick && handleClick()}>
<div className="link">
<link.icon className="icon" />
{link.name}
</div>
</NavLink>
))}
</div>
)
And then with that component I just do something like
<NavLinks handleClick={() => setMenuState(false)} />
How can I best type this so it stops giving the error in the title? I'm not clear why it would expect there's a value when I'm typed it to be something that sets state?
I see stuff online that, more often than not, is assuming the onClick is going to apply to an HTML button element, but I'm just using this to click on react-icons, so I'm even more lost.
handleClick should be of type () => void since setMenuState is wrapped in a function.
interface IProps {
handleClick: () => void;
icon?: JSX.Element;
}
If you passed setMenuState directly like:
<NavLinks handleClick={setMenuState} />
then it can be typed as a setState function

Force react component to only accept a child with a specific type

I have this following component:
interface InputWithButtonProps {
label: string;
children: React.ReactElement<typeof CustomButton>;
}
const InputWithButton = ({
label,
fieldName,
disabled,
children,
}: InputWithButtonProps): JSX.Element => {
return (
<>
<TextField label={label} />
{Children.only(children)}
</>
);
};
export default InputWithButton;
And the code for CustomButton is:
export declare const CustomButton: import("#material-ui/core").ExtendButtonBase<import("#material-ui/core").ButtonTypeMap<{}, "button">>;
What I want from InputWithButton is to only accept one single child of type CustomButton, but specifying the type using React.ReactElement<typeof CustomButton> doesn't do any checks, so the following code will work just fine (Which should throw an error in this case):
<InputWithButton label="some label">
<div>test</div>
</InputWithButton>
The InputWithButton should only accept this:
<InputWithButton label="some label">
<Button>Some button</Button>
</InputWithButton>
I also tried to specify the children type as following:
children: React.ReactElement<ButtonProps>;
Which didn't work either.
How can I solve this?
You need to apply the condition that checks if the children's type is Button or not, consider the code below that will solve your problem if I understand your problem correctly,
interface InputWithButtonProps {
label: string;
children: React.ReactElement<typeof CustomButton>;
}
const InputWithButton = ({
label,
fieldName,
disabled,
children,
}: InputWithButtonProps): JSX.Element => {
return typeof children.type == 'function' &&
children.type.name == "Button" &&
(
<>
<input label={label} />
{children}
</>
);
};
export default InputWithButton;
The component will return nothing if the tag is other than 'Button', you can remove the 'typeof children.type' condition because it only checks if the children is custom made component or an HTML tag.

Render an element based on a given prop taking on appropriate element attributes

I would like to create a component that renders a specific HTML element based on properties given to it. In this case, I'd like to render a div if the component's isDiv property is true, and a button if it's false.
I also want to be able to provide my component with any of the element's attributes, which will be passed on down to the element itself.
Without TypeScript, I might write the component like so:
const Button = ({ isDiv, elementProps, children }) => {
return isDiv ? (
<div {...elementProps} className="button">{children}</div>
) : (
<button {...elementProps} className="button">{children}</button>
);
};
To be used, for example, like:
<Button type="submit" />
{/* <button class="button" type="button">...</button> */}
<Button isDiv />
{/* <div class="button">...</div> */}
My attempt now, using TypeScript (and a technique I've read refered to as a "Discriminated Union") is as follows:
type DivProps = {
isDiv: true;
elementProps: React.HTMLAttributes<HTMLDivElement>
};
type ButtonProps = {
isDiv: false;
elementProps: React.ButtonHTMLAttributes<HTMLButtonElement>;
};
const Button: React.FC<DivProps | ButtonProps> = ({
isDiv,
elementProps,
children,
}) => {
return isDiv ? (
<div {...elementProps} className="button">{children}</div>
) : (
<button {...elementProps} className="button">{children}</button>
);
};
Where I get errors due to HTMLButtonElement and HTMLDivElement not being compatible, ultimately:
Property 'align' is missing in type 'HTMLButtonElement' but required in type 'HTMLDivElement'
How can I correctly implement this component using TypeScript?
you need to help TS know about the relationship between isDiv and elementProps so it could narrow down the discriminated union.
this works:
const Button: React.FC<DivProps | ButtonProps> = ({
children,
...props,
}) => {
return props.isDiv ? (
<div {...props.elementProps} className="button">{children}</div>
) : (
<button {...props.elementProps} className="button">{children}</button>
);
};

React with TypeScript: how to create ref prop

I am using Ionic with React (typescript) and I am creating my custom form builder. There I created my form that has to have a ref property, because I need a reference of it when I use it.
My problem is that I don't know how to define a prop that is reference type for my custom form builder.
This is how I am using it:
const form = useRef<HTMLFormElement>(null);
return (
<IonPage>
<Header />
<IonContent fullscreen>
<Form
ref={form}
submit={() => onSubmit()}
fields={ fields }
render={() => (
<React.Fragment>
<IonCard>
<IonCardHeader>
<IonLabel>Valamilyen form</IonLabel>
</IonCardHeader>
<IonCardContent>
<Field {...fields.name} />
</IonCardContent>
</IonCard>
</React.Fragment>
)}/>
</IonContent>
<Footer />
</IonPage>
);
Here I got an error:
Property 'ref' does not exist on type 'IntrinsicAttributes & IFormProps & IFormState & { children?: ReactNode; }'
My Form React.FC looks like this:
type formProps = IFormProps & IFormState;
export const Form: React.FC<formProps> = React.forwardRef<HTMLFormElement, formProps>( (props: formProps, porpsRef) => {
return (
<form onSubmit={handleSubmit} noValidate={true} ref={porpsRef} />
);
)};
I need to add to my Form component a property named ref of the reference, but I don't know how.
Thanks
I think there is small mistake. Can you please try this
type formProps = IFormProps & IFormState;
export const Form: React.FC<formProps> =
React.forwardRef<formProps, HTMLFormElement>( (props:
formProps, porpsRef) =>
{
return (
<form onSubmit={handleSubmit} noValidate={true} ref=
{porpsRef} />
);
)};
I just typed a very long answer to a similar question. In your case you seem to be making only one of those mistakes, which is declaring your Form as React.FC. The FC interface isn't aware of the ref forwarding that you've added with React.forwardRef so that's why you get an error when trying to pass a prop ref to it.
Delete this bit : React.FC<formProps> and you should be fine. You've already typed the generics on the React.forwardRef function, so typescript will know the return type and apply it to Form.
export const Form = React.forwardRef<HTMLFormElement, formProps>(...
If you wanted to explicitly declare the type, you can do that but it's a mouthful.
export const Form: React.ForwardRefExoticComponent<formProps & React.RefAttributes<HTMLFormElement>> = ...
(also you've forgotten to destructure handleSubmit from formProps)

ReactJS typescript error Parameter props implicitly has 'any' type

Following, and adapting the tutorial here, I've hit a snag when trying to define a function to render some HTML.
function Toolbar(props) {
return (
<div>
<button onClick={() => props.onClick()}>Refresh</button>
</div>
);
}
This errors in Typescript, because props is not defined. I understand why I'm getting the error, but what I'm missing is what type should props be?
I believe I should then be able to use this inside another component like this:
function App() {
return (
<div>
<Toolbar onClick={() => alert('hello world')}>Test</Toolbar>
</div>
);
}
And the props is just whatever I specify. This would seem to be borne out here: although that link doesn't mention typescript. So, my question is: how does this feature (that of passing properties into a function or class) work with typescript when, by definition, you don't know what you're passing through?
You should define an interface for the props.
interface Props {
onClick: () => void;
}
function Toolbar(props: Props) {
return (
<div>
<button onClick={() => props.onClick()}>Refresh</button>
</div>
);
}
I also like to define my functional components as an arrow function. And you can use the FC (Functional Component) type definition with the Props generic type. This way you can deconstruct your properties right away.
const Toolbar: React.FC<Props> = ({onClick}) => {
return (
<div>
<button onClick={() => onClick()}>Refresh</button>
</div>
);
}

Resources