ReactHustle

How to Extend HTML Elements in React (Typescript)

Jasser Mark Arioste

Jasser Mark Arioste

How to Extend HTML Elements in React (Typescript)

In one of my projects I often ended up extending html elements when using a css library like daisyUI. DaisyUI is great because it's so flexible and it uses html elements only. It's not like MaterialUI where a <button> has it's own react component and has a lot of built-in functionality.

In other words, sometimes we want to create a <Button> component that gets all the properties of <button> and has extra functionalities or props.

Prerequisites:

  1. Basic knowledge of generics in typescript
  2. Basic knowledge of react components

Wrapping an HTML Element into a component #

If we need to wrap an html element we can simply extend the generic interface React.ComponentPropsWithoutRef<T> for our props and pass it onto our component. For example if we want to create a button with a start icon in daisyUI. This is a overly simplified example:

//Button.tsx

import React from "react";

//create the button Props
export interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
  startIcon?: React.ReactNode;
  //write other extra props here...
}

const Button = ({ startIcon, children, ...props }: ButtonProps) => {
  //render the button
  return (
    <button {...props} className="btn gap-2">
      {startIcon}
      {children}
    </button>
  );
};

export default Button;

123456789101112131415161718192021

And how to use it:


import Button from "../components/Button";
import { FiDownload } from "react-icons/fi";
const Component = () => {
  return (
    <div className="container mx-auto my-4">
      <Button>
        <FiDownload size={20} />
        Download
      </Button>
    </div>
  );
};

export default Component;
123456789101112131415

Result:

Button component after extending the button element

What are the cons of wrapping an html element? #

Everything above looks good however we should always weigh the pros and cons. If we merely want to add an icon to a button, we can simply do without a extra layer of component. 

import { FiDownload } from "react-icons/fi";
...
<button className="btn gap2">
  <FiDownload size={20} />
  Download
</button>

...
12345678

Below are the cons of wrapping html elements:

  1. Every wrapped html element is much slower to render compared to regular html + classes. It might be negligible but this is just a thing to keep in mind.
  2. Every layer of abstraction creates a constraint that will be harder to maintain in the future. It creates restrictions and less flexibility. 

When should you extend html elements or components? #

You should extend html elements when the component is widely used in the application and has a very different behavior that is hard than just regular html + classes. One example is if we want to add special effects like "ripple effect" to a button.

//ButtonWithRipple.tsx
import React, { CSSProperties, useEffect, useState } from "react";
import { useDebounce } from "usehooks-ts";

//create the button Props
export interface ButtonWithRipple
  extends React.ComponentPropsWithoutRef<"button"> {}

const ButtonWithRipple = ({
  children,
  onClick,
  className,
  ...props
}: ButtonWithRipple) => {
  const [ripples, setRipples] = useState<CSSProperties[]>([]);
  const _debounced = useDebounce(ripples, 2000);
  const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
    var rect = e.currentTarget.getBoundingClientRect();
    var left = e.clientX - rect.left;
    var top = e.clientY - rect.top;
    const height = e.currentTarget.clientHeight;
    const width = e.currentTarget.clientWidth;
    const diameter = Math.max(width, height);
    setRipples([
      ...ripples,
      {
        top: top - diameter / 2,
        left: left - diameter / 2,
        height: Math.max(width, height),
        width: Math.max(width, height),
      },
    ]);

    if (onClick) {
      onClick(e);
    }
  };

  useEffect(() => {
    if (_debounced.length) {
      setRipples([]);
    }
  }, [_debounced.length]);

  //render the button
  return (
    <button
      {...props}
      onClick={handleClick}
      className={className + " relative no-animation overflow-hidden"}
    >
      {ripples.map((ripple, index) => {
        return (
          <div
            key={index}
            className="absolute bg-gray-50 rounded-full opacity-25 ripple"
            style={ripple}
          ></div>
        );
      })}
      {children}
    </button>
  );
};

export default ButtonWithRipple;
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566

Add ripple animation:


.ripple {
  transform: scale(0);
  animation: ripple 600ms linear;
}

@keyframes ripple {
  to {
    transform: scale(4);
    opacity: 0;
  }
}
123456789101112

Usage:


<ButtonWithRipple className="btn gap-2">
  <FiDownload size={20} />
  Download
</ButtonWithRipple>
12345
Button with ripple effects using tailwind and react.

Conclusion #

We successfully created a component that extends the default html button. We also learned on what are the cons of extending html elements and when to extend an html element to a react component that we can use anywhere in our application.

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle