Sponsored

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
Sponsored

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
Sponsored

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. 
Sponsored

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.
Sponsored

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

Sponsored

Subscribe to our newsletter

Get up-to-date on latest articles on react.js, node.js, graphql, full-stack web development. No Spam. Helpful articles to improve your skills. Sent directly to your inbox.