How to Extend HTML Elements in React (Typescript)
Jasser Mark Arioste
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:
- Basic knowledge of generics in typescript
- 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:
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:
- 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.
- 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
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.