Extending HTML elements in React and TypeScript while preserving props
ReactjsTypescriptTsxReactjs Problem Overview
I just can't wrap my head around this I guess, I've tried probably half a dozen times and always resort to any
... Is there a legitimate way to start with an HTML element, wrap that in a component, and wrap that in another component such that the HTML props pass through everything? Essentially customizing the HTML element? For example, something like:
interface MyButtonProps extends React.HTMLProps<HTMLButtonElement> {}
class MyButton extends React.Component<MyButtonProps, {}> {
render() {
return <button/>;
}
}
interface MyAwesomeButtonProps extends MyButtonProps {}
class MyAwesomeButton extends React.Component<MyAwesomeButtonProps, {}> {
render() {
return <MyButton/>;
}
}
Usage:
<MyAwesomeButton onClick={...}/>
Whenever I attempt this sort of composition, I get an error similar to:
> Property 'ref' of foo is not assignable to target property.
Reactjs Solutions
Solution 1 - Reactjs
You can change the definition of your component to allow the react html button props
class MyButton extends React.Component<MyButtonProps & React.HTMLProps<HTMLButtonElement>, {}> {
render() {
return <button {...this.props}/>;
}
}
That will tell the typescript compiler that you want to enter the button props along with 'MyButtonProps'
Solution 2 - Reactjs
I always like to do it this way:
import React from 'react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
title: string;
showIcon: boolean;
}
const Button: React.FC<ButtonProps> = ({ title, showIcon, ...props }) => {
return (
<button {...props}>
{title}
{showIcon && <Icon/>}
</button>
);
};
Then you can do:
<Button
title="Click me"
onClick={() => {}} {/* You have access to the <button/> props */}
/>
Solution 3 - Reactjs
Seems Like the above answer is outdated.
In my case I'm wrapping a styled component with a functional component, but still want to expose regular HTML button properties.
export const Button: React.FC<ButtonProps &
React.HTMLProps<HTMLButtonElement>> = ({
...props,
children,
icon
}) => (
<StyledButton {...props}>
{icon && <i className="material-icons">{icon}</i>}
{children}
</StyledButton>
);
Solution 4 - Reactjs
This worked for my by using a type (instead of an interface):
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
children: React.ReactNode;
icon?: React.ReactNode;
};
function Button({ children, icon, ...props }: ButtonProps) {
return (
<button {...props}>
{icon && <i className="icon">{icon}</i>}
{children}
</button>
);
}
Solution 5 - Reactjs
This is what I do when extending native elements:
import React, { ButtonHTMLAttributes, forwardRef } from "react";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
myExtraProp1: string;
myExtraProp2: string;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ myExtraProp1, myExtraProp2, ...props }, ref) => (
<button
{...props}
ref={ref}
// Do something with the extra props
/>
),
);
Button.displayName = "Button";
forwardRef
ensures that you can get a reference to the underlying HTML element with ref
when using the component.
Solution 6 - Reactjs
I solve this code for me, you just have to import ButtonHTMLAttributes
from react and that's it
import { ButtonHTMLAttributes } from "react";
interface MyButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: any;
}
export const MyButton = (props: ButtonI) => {
const { children } = props;
return <button {...props}>{children}</button>;
};
Solution 7 - Reactjs
if you're using styled components from '@emotion/styled', none of the answers work.
I had to go a little deeper.
import styled from "@emotion/styled";
import React, { ButtonHTMLAttributes } from 'react';
export type ButtonVariant = 'text' | 'filled' | 'outlined';
export const ButtonElement = styled.button`
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px;
`;
export interface ButtonProps {
variant: ButtonVariant;
}
export const Button: React.FC<ButtonProps & React.DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>> = ({
children,
variant,
...props
}) => (
<ButtonElement
{...props}
>
{children}
</ButtonElement>
);
this style allows you to pass all props that button has, and more than that, padding {...props} to ButtonElement allows you to easily reuse Button with styled-components, to do css changes you want in a good way
import { Button } from '@components/Button';
export const MySpecificButton = styled(Button)`
color: white;
background-color: green;
`;
Solution 8 - Reactjs
private yourMethod(event: React.MouseEvent<HTMLButtonElement>): void {
event.currentTarget.disabled = true;
}
<Button
onClick={(event) => this.yourMethod(event)}
/>
Solution 9 - Reactjs
I encountered the same issue today and here is how I fixed it:
ReactButtonProps.ts
import {
ButtonHTMLAttributes,
DetailedHTMLProps,
} from 'react';
/**
* React HTML "Button" element properties.
* Meant to be a helper when using custom buttons that should inherit native "<button>" properties.
*
* @example type MyButtonProps = {
* transparent?: boolean;
* } & ReactButtonProps;
*/
export type ReactButtonProps = DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
Usage in Button-ish
component:
import classnames from 'classnames';
import React, { ReactNode } from 'react';
import { ReactButtonProps } from '../../types/react/ReactButtonProps';
type Props = {
children: ReactNode;
className?: string;
mode?: BtnMode;
transparent?: boolean;
} & ReactButtonProps;
const BtnCTA: React.FunctionComponent<Props> = (props: Props): JSX.Element => {
const { children, className, mode = 'primary' as BtnMode, transparent, ...rest } = props;
// Custom stuff with props
return (
<button
{...rest} // This forward all given props (e.g: onClick)
className={classnames('btn-cta', className)}
>
{children}
</button>
);
};
export default BtnCTA;
Usage:
<BtnCTA className={'test'} onClick={() => console.log('click')}>
<FontAwesomeIcon icon="arrow-right" />
{modChatbot?.homeButtonLabel}
</BtnCTA>
I can now use onClick
because it's allowed due to extending from ReactButtonProps, and it's automatically forwarded to the DOM through the ...rest
.
Solution 10 - Reactjs
Extend HTML Element with Ref & Key
If you need to be able to accept TL;DR
ref
and key then your type definition will need to use this long ugly thing:
import React, { DetailedHTMLProps, HTMLAttributes} from 'react';
DetailedHTMLProps<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
Looking at the type definition file, this is the type. I'm not sure why it isn't shorter, it seems you always pass the same HTMLElement twice? Type Definition
type DetailedHTMLProps<E extends HTMLAttributes<T>, T> = ClassAttributes<T> & E;
Shortened DetailedHTMLProps
You could create your own type to shorten this for our case (which seems to be the common case).
import React, { ClassAttributes, HTMLAttributes} from 'react';
type HTMLProps<T> = ClassAttributes<T> & HTMLAttributes<T>;
export interface ButtonProps extends HTMLProps<HTMLButtonElement> {
variant: 'contained' | 'outlined';
}
Sample Component
import React, {ClassAttributes, HTMLAttributes, ForwardedRef, forwardRef} from 'react';
type HTMLProps<T> = ClassAttributes<T> & HTMLAttributes<T>;
export interface ButtonProps extends HTMLProps<HTMLButtonElement> {
variant: 'contained' | 'outlined';
}
export const Button: React.FC<ButtonProps> = forwardRef(
(props : ButtonProps, ref: ForwardedRef<HTMLButtonElement>) => {
return (
<button key="key is accepted" ref={ref} {...props}>
{props.children}
</button>
);
},
);
Solution 11 - Reactjs
You can do this to extend the button properties
import { ButtonHTMLAttributes, ReactNode } from "react";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
}
const Button = ({ children, ...props }: Props): JSX.Element => {
return <button {...props}>{children}</button>;
};