Extending HTML elements in React and TypeScript while preserving props
Asked Answered
V

16

163

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.

Verile answered 21/11, 2016 at 23:29 Comment(6)
You're looking for High Order Components (component factories). Check em out online and see if that fits what you're asking for. They're essentially "component factories" that will allow you to wrap a component in another component which returns that initial component, but with new or modified props.Glasscock
Is the error compile time (when you compile)? Because, I tried to compile your code using tsc command and works fine. I tried to render <MyAwesomeButton onClick={() => console.log('Clicked')}/>Incrassate
One thing I noticed is that, shouldn't you pass the props to your native (HTML) element as <button {...this.props} /> exactly?Incrassate
This thread discusses an issue with some of the proposed answers github.com/DefinitelyTyped/DefinitelyTyped/issues/36505 and suggests interface Props extends React.ComponentProps<'button'> to capture any missing props.Incentive
I always use React.ComponentsProps<"button"> the generic input can be anything from a react component to a string such as "div". There are also the variants ComponentsPropsWithRef and ComponentsPropsWithoutRef to use when working with or without ref forwarding.Yoohoo
In React 18 extending React.InputHTMLAttributes<HTMLInputElement> will provide more predictable results, like e.target.value not throwing type errors in onChangeBenediction
P
181

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 */}
/>
Poliard answered 25/6, 2020 at 6:4 Comment(0)
C
157

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'

Colligan answered 14/8, 2017 at 15:25 Comment(0)
H
67

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>> = ({
  children,
  icon,
  ...props,
}) => (
  <StyledButton {...props}>
    {icon && <i className="material-icons">{icon}</i>}
    {children}
  </StyledButton>
);
Hobie answered 16/4, 2020 at 9:54 Comment(0)
K
11

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>
  );
}
Kris answered 13/11, 2021 at 15:45 Comment(0)
E
8

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.

Eudemonia answered 26/3, 2021 at 3:44 Comment(0)
M
8

Most concise method

This is the method I use everytime I want to extend an HTML element.

import { JSX } from 'react';

type ButtonProps = JSX.IntrinsicElements['button']
type DivProps = JSX.IntrinsicElements['div']
Mammalogy answered 14/3, 2023 at 13:46 Comment(2)
Nice, this worked well. To import JSX, you can just do it from "react", i.e. import { JSX } from "react"Clown
with this approach button props works good, but props.children gives error.Organon
L
5
import * as React from "react";

interface Props extends React.HTMLProps<HTMLInputElement> {
  label?: string;
}

export default function FormFileComponent({ label, ...props }: Props) {
  return (
    <div>
      <label htmlFor={props?.id}></label>
      <input type="file" {...props} />
    </div>
  );
}
Lubricity answered 27/6, 2022 at 19:13 Comment(1)
you should not use HTMLProps react-typescript-cheatsheet.netlify.app/docs/advanced/…Vasquez
V
4

Extend HTML Element with Ref & Key

TL;DR
If you need to be able to accept `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>
Type Definition
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 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 = forwardRef<HTMLButtonElement, ButtonProps>(
  (props, ref) => {

    return (
      <button key="key is accepted" ref={ref} {...props}>
        {props.children}
      </button>
    );
  },
);
Volga answered 8/1, 2022 at 23:47 Comment(1)
see here why you shouldnt extend HTMLElement react-typescript-cheatsheet.netlify.app/docs/advanced/…Vasquez
Y
3

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;
`;
Yolanthe answered 20/10, 2021 at 1:40 Comment(0)
B
3

You need extend your interface.

import {ButtonHTMLAttributes, ReactNode} from "react";

export interface ButtonProps extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>{
    appearance: 'primary' | 'ghost';
    children: ReactNode;
}
Beata answered 31/8, 2022 at 16:16 Comment(0)
T
2

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>;
};
Triplett answered 7/12, 2020 at 3:25 Comment(0)
R
1
import {  FC, HTMLProps } from 'react';

const Input: FC<HTMLProps<HTMLInputElement>> = (props) => {
  return <input {...props} />;
};
Repurchase answered 28/7, 2022 at 0:54 Comment(0)
S
0
  private yourMethod(event: React.MouseEvent<HTMLButtonElement>): void {
  event.currentTarget.disabled = true;
  }  

 <Button
 onClick={(event) => this.yourMethod(event)}
 />
Stockholm answered 6/7, 2020 at 9:4 Comment(0)
V
0

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.

Virgate answered 8/10, 2020 at 12:12 Comment(0)
G
0

I got this to work. Somehow, extending the props by using the HtmlHTMLAttributes did not work on my end. What worked was using the ComponentPropsWithoutRef like below:

import clsx from "clsx"
import { FC } from "react";

 interface InputProps
  extends React.ComponentPropsWithoutRef<'input'>{
    className?: string;
  }

const Input: FC<InputProps> = ({ className, ...props}) => {
  return (
    <input
    className={clsx('border-solid border-gray border-2 px-6 py-2 text-lg rounded-3xl w-full', className)}
     {...props} />
  )
}

export default Input

Then I could use the usual Input props as such:

              <Input
                required
                placeholder="First Name"
                value={formState.firstName}
                className="border-solid border-gray border-2 px-6 py-2 text-lg rounded-3xl w-full"
                onChange={(e) =>
                  setFormState((s) => ({ ...s, firstName: e.target.value }))
                }
              />
   
Geyer answered 13/6, 2023 at 15:11 Comment(1)
If anyone has an explanation on why would be greatly appreciated.Geyer
Y
0

Easy way for any element

interface YOUR_INTERFACE_NAME extends YOUR_ELEMENT_TYPE{
props which you want to add or use in other elements
}

EXAMPLE FOR INPUT

interface MyInput extends InputHTMLAttributes<HTMLInputElement> {
name: string;
label: string;
...
}

EXAMPLE FOR BUTTON

interface MyButton extends ButtonHTMLAttributes<HTMLButtonElement> {
name: string;
label: string;
...
}

USE SAME PATTERN FOR ALL OTHER ELEMENTS -- import those types from react or just add REACT. before them

Yuri answered 16/4 at 17:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.